Companion code · Chapter 20

A First Look at Concurrency

17 files. About this chapter.

Download ch20.zip

ch20/check_then_act.cpp32 lines
// check_then_act.cpp: every access locked, still wrong
#include <cstdio>
#include <mutex>
#include <thread>

class LockedCount {
public:
    long get()       { std::lock_guard<std::mutex> g(m_);
                       return n_; }
    void set(long v) { std::lock_guard<std::mutex> g(m_);
                       n_ = v; }
private:
    std::mutex m_;
    long n_ = 0;
};

LockedCount count;

void add_many(long times) {
    for (long i = 0; i < times; ++i) {
        count.set(count.get() + 1);   // two locked steps
    }
}

int main(void) {
    std::thread a(add_many, 100000);
    std::thread b(add_many, 100000);
    a.join();
    b.join();
    std::printf("%ld\n", count.get());
    return 0;
}
ch20/counter_atomic.cpp23 lines
// counter_atomic.cpp: each increment is indivisible
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <thread>

std::atomic<long> counter{0};  // each ++ is indivisible

void add_many(long times) {
    for (long i = 0; i < times; ++i) {
        ++counter;             // atomic read-modify-write
    }
}

int main(int argc, char* argv[]) {
    long times = (argc > 1) ? std::atol(argv[1]) : 1000;
    std::thread a(add_many, times);
    std::thread b(add_many, times);
    a.join();
    b.join();
    std::printf("%ld\n", counter.load());
    return 0;
}
ch20/counter_mutex.cpp25 lines
// counter_mutex.cpp: the same counter, protected by a mutex
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <thread>

long counter = 0;          // shared; guarded by counter_mutex
std::mutex counter_mutex;      // protects counter, nothing else

void add_many(long times) {
    for (long i = 0; i < times; ++i) {
        std::lock_guard<std::mutex> guard(counter_mutex);
        ++counter;             // the critical section
    }                          // guard destroyed: unlock
}

int main(int argc, char* argv[]) {
    long times = (argc > 1) ? std::atol(argv[1]) : 1000;
    std::thread a(add_many, times);
    std::thread b(add_many, times);
    a.join();
    b.join();
    std::printf("%ld\n", counter);
    return 0;
}
ch20/counter_race.cpp22 lines
// counter_race.cpp: two threads increment one shared counter
#include <cstdio>
#include <cstdlib>
#include <thread>

long counter = 0;              // shared, unprotected

void add_many(long times) {
    for (long i = 0; i < times; ++i) {
        ++counter;             // read, add one, write back
    }
}

int main(int argc, char* argv[]) {
    long times = (argc > 1) ? std::atol(argv[1]) : 1000;
    std::thread a(add_many, times);
    std::thread b(add_many, times);
    a.join();
    b.join();
    std::printf("%ld\n", counter);
    return 0;
}
ch20/lock_cost.cpp87 lines
// lock_cost.cpp: count readings above 25.0 C six ways and time each
#include <atomic>
#include <chrono>
#include <cstdio>
#include <mutex>
#include <thread>
#include <vector>

const std::size_t N = 10'000'000;
const double THRESHOLD = 25.0;
std::vector<double> temps(N);

long shared_count = 0;
std::mutex count_mutex;
std::atomic<long> atomic_count{0};

// Each worker scans temps[lo, hi).
void scan_plain(std::size_t lo, std::size_t hi, long* out) {
    long local = 0;
    for (std::size_t i = lo; i < hi; ++i) {
        if (temps[i] > THRESHOLD) ++local;
    }
    *out = local;                       // private result, no sharing
}
void scan_atomic(std::size_t lo, std::size_t hi) {
    for (std::size_t i = lo; i < hi; ++i) {
        if (temps[i] > THRESHOLD) ++atomic_count;  // atomic per hit
    }
}
void scan_mutex(std::size_t lo, std::size_t hi) {
    for (std::size_t i = lo; i < hi; ++i) {
        if (temps[i] > THRESHOLD) {
            std::lock_guard<std::mutex> g(count_mutex);
            ++shared_count;             // lock per hit
        }
    }
}
void scan_local_then_lock(std::size_t lo, std::size_t hi) {
    long local = 0;
    for (std::size_t i = lo; i < hi; ++i) {
        if (temps[i] > THRESHOLD) ++local;
    }
    std::lock_guard<std::mutex> g(count_mutex);
    shared_count += local;              // one lock per thread
}

template <typename F>
double best_ms(F f) {
    double best = 1e9;
    for (int rep = 0; rep < 11; ++rep) {
        shared_count = 0;
        atomic_count = 0;
        auto t0 = std::chrono::steady_clock::now();
        f();
        auto t1 = std::chrono::steady_clock::now();
        double ms = std::chrono::duration<double, std::milli>(t1 - t0).count();
        if (ms < best) best = ms;
    }
    return best;
}

int main(void) {
    for (std::size_t i = 0; i < N; ++i) {
        temps[i] = 20.0 + static_cast<double>((i * 7919) % 1000) / 100.0;
    }
    long r1 = 0;
    double a = best_ms([&] { scan_plain(0, N, &r1); });
    std::printf("1 thread, no lock:          %7.1f ms  count=%ld\n", a, r1);
    double b = best_ms([&] { scan_mutex(0, N); });
    std::printf("1 thread, mutex per hit:    %7.1f ms  count=%ld\n", b, shared_count);
    double b2 = best_ms([&] { scan_atomic(0, N); });
    std::printf("1 thread, atomic per hit:   %7.1f ms  count=%ld\n", b2, atomic_count.load());
    double c = best_ms([&] {
        std::thread t1(scan_mutex, 0, N / 2), t2(scan_mutex, N / 2, N);
        t1.join(); t2.join(); });
    std::printf("2 threads, mutex per hit:   %7.1f ms  count=%ld\n", c, shared_count);
    double d = best_ms([&] {
        std::thread t1(scan_atomic, 0, N / 2), t2(scan_atomic, N / 2, N);
        t1.join(); t2.join(); });
    std::printf("2 threads, atomic per hit:  %7.1f ms  count=%ld\n", d, atomic_count.load());
    double e = best_ms([&] {
        std::thread t1(scan_local_then_lock, 0, N / 2),
                    t2(scan_local_then_lock, N / 2, N);
        t1.join(); t2.join(); });
    std::printf("2 threads, local then lock: %7.1f ms  count=%ld\n", e, shared_count);
    return 0;
}
ch20/lost_wakeup.cpp29 lines
// lost_wakeup.cpp: the sensor finishes before the writer waits
#include <chrono>
#include <cstdio>
#include <thread>
#ifdef FIXED
#include "ring_buffer.h"
#else
#include "ring_buffer_lost.h"
#endif

int main(void) {
    stn::RingBuffer buf(64);
    std::thread sensor([&] {
        for (int t = 0; t < 3; ++t) buf.push({1001, t * 60, 21.0, 1});
        buf.close();                      // three readings, then done
    });
    std::thread writer([&] {
        std::this_thread::sleep_for(      // busy elsewhere for 100 ms
            std::chrono::milliseconds(100));
        stn::Reading r;
        while (buf.pop_wait(r)) {
            std::printf("wrote t=%lld\n", static_cast<long long>(r.time_s));
        }
        std::printf("writer done\n");
    });
    sensor.join();
    writer.join();
    return 0;
}
ch20/no_join.cpp11 lines
// no_join.cpp: a std::thread destroyed while still joinable
#include <cstdio>
#include <thread>

void hello() { std::printf("hello from the thread\n"); }

int main(void) {
    std::thread t(hello);
    std::printf("main returns without join\n");
    return 0;                       // t's destructor runs here
}
ch20/overlap.cpp33 lines
// overlap.cpp: how long do 1,000 increments take, compared with
// starting a thread? (Book build line; times are rough.)
#include <chrono>
#include <cstdio>
#include <thread>

long counter = 0;
using clk = std::chrono::steady_clock;

void add_many(long times) {
    for (long i = 0; i < times; ++i) ++counter;
}

double us_since(clk::time_point t0, clk::time_point t1) {
    return std::chrono::duration<double, std::micro>(t1 - t0).count();
}

int main(void) {
    for (long n : {1000L, 100000L}) {
        auto t0 = clk::now();
        add_many(n);
        auto t1 = clk::now();
        std::printf("n=%ld loop %.1f us\n", n, us_since(t0, t1));
    }
    for (int k = 0; k < 5; ++k) {
        clk::time_point started;
        auto t0 = clk::now();
        std::thread t([&] { started = clk::now(); });
        t.join();
        std::printf("start latency %.1f us\n", us_since(t0, started));
    }
    return 0;
}
ch20/ring_buffer.h40 lines
#pragma once
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <vector>
namespace stn {
struct Reading { std::uint32_t sensor_id; std::int64_t time_s;
                 double value; std::uint8_t flags; };
class RingBuffer {
public:
    explicit RingBuffer(std::size_t cap) : data_(cap) {}
    bool push(const Reading& r) {           // sensor thread
        std::unique_lock<std::mutex> lock(m_);
        if (count_ == data_.size()) return false;   // full
        data_[(head_ + count_) % data_.size()] = r;
        ++count_;
        lock.unlock();                      // unlock, then wake
        not_empty_.notify_one();
        return true;
    }
    bool pop_wait(Reading& out) {           // writer thread
        std::unique_lock<std::mutex> lock(m_);
        not_empty_.wait(lock, [this] {  // re-checked on wake
            return count_ > 0 || closed_; });
        if (count_ == 0) return false;  // closed and drained
        out = data_[head_];
        head_ = (head_ + 1) % data_.size(); --count_;
        return true;
    }
    void close() { std::lock_guard<std::mutex> lock(m_);
                   closed_ = true; not_empty_.notify_all(); }
private:
    std::mutex m_;                          // guards all below
    std::condition_variable not_empty_;
    std::vector<Reading> data_;
    std::size_t head_ = 0, count_ = 0;
    bool closed_ = false;
};
}  // namespace stn
ch20/ring_buffer_agent.h35 lines
// ring_buffer_agent.h: the agent's "thread-safe" buffer
#pragma once
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <vector>
namespace stn {
struct Reading { std::uint32_t sensor_id; std::int64_t time_s;
                 double value; std::uint8_t flags; };
class RingBuffer {
public:
    explicit RingBuffer(std::size_t cap) : data_(cap) {}
    bool push(const Reading& r) {           // write: locked
        std::lock_guard<std::mutex> lock(m_);
        if (count_ == data_.size()) return false;   // full
        data_[(head_ + count_) % data_.size()] = r;
        ++count_;
        return true;
    }
    bool pop(Reading& out) {                // write: locked
        std::lock_guard<std::mutex> lock(m_);
        if (count_ == 0) return false;
        out = data_[head_];
        head_ = (head_ + 1) % data_.size(); --count_;
        return true;
    }
    std::size_t size() const { return count_; }  // no lock
    bool empty() const { return count_ == 0; }   // no lock
private:
    std::mutex m_;
    std::vector<Reading> data_;
    std::size_t head_ = 0;     // index of the oldest reading
    std::size_t count_ = 0;    // readings currently stored
};
}  // namespace stn
ch20/ring_buffer_lost.h39 lines
#pragma once
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <vector>
namespace stn {
struct Reading { std::uint32_t sensor_id; std::int64_t time_s;
                 double value; std::uint8_t flags; };
class RingBuffer {
public:
    explicit RingBuffer(std::size_t cap) : data_(cap) {}
    bool push(const Reading& r) {           // sensor thread
        std::unique_lock<std::mutex> lock(m_);
        if (count_ == data_.size()) return false;   // full
        data_[(head_ + count_) % data_.size()] = r;
        ++count_;
        lock.unlock();                      // unlock, then wake
        not_empty_.notify_one();
        return true;
    }
    bool pop_wait(Reading& out) {           // writer thread
        std::unique_lock<std::mutex> lock(m_);
        not_empty_.wait(lock);              // wait for a notify
        if (count_ == 0) return false;  // closed and drained
        out = data_[head_];
        head_ = (head_ + 1) % data_.size(); --count_;
        return true;
    }
    void close() { std::lock_guard<std::mutex> lock(m_);
                   closed_ = true; not_empty_.notify_all(); }
private:
    std::mutex m_;                          // guards all below
    std::condition_variable not_empty_;
    std::vector<Reading> data_;
    std::size_t head_ = 0, count_ = 0;
    bool closed_ = false;
};
}  // namespace stn
ch20/slow_io.cpp58 lines
// slow_io.cpp: how long does the sensor wait for the lock?
#include <atomic>
#include <chrono>
#include <cstdio>
#include <mutex>
#include <thread>
#include <utility>
#include <vector>

using clk = std::chrono::steady_clock;
std::mutex m;                       // guards batch
std::vector<double> batch;
std::atomic<bool> done{false};

void slow_write(const std::vector<double>& v) {   // the SD card
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    (void)v;
}

void writer(bool hold_lock) {           // flushes every 20 ms
    while (!done) {
        std::vector<double> mine;
        {
            std::lock_guard<std::mutex> lock(m);
            if (hold_lock) slow_write(batch); // slow, locked
            std::swap(mine, batch);           // fast, locked
        }
        if (!hold_lock) slow_write(mine);     // slow, unlocked
        std::this_thread::sleep_for(
            std::chrono::milliseconds(20));
    }
}

double sensor_worst_wait_ms() {     // 300 readings, one per ms
    double worst = 0.0;
    for (int i = 0; i < 300; ++i) {
        auto t0 = clk::now();
        { std::lock_guard<std::mutex> lock(m); batch.push_back(21.0); }
        std::chrono::duration<double, std::milli> w = clk::now() - t0;
        if (w.count() > worst) worst = w.count();
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    return worst;
}

int main(void) {
    for (int mode = 0; mode < 2; ++mode) {
        done = false;
        std::thread w(writer, mode == 0);
        double worst = sensor_worst_wait_ms();
        done = true;
        w.join();
        std::printf("%s worst push wait: %.3f ms\n",
                    mode == 0 ? "lock across write:" : "swap, then write: ",
                    worst);
    }
    return 0;
}
ch20/small_task.cpp40 lines
// small_task.cpp: is it worth starting a thread for 1,440 readings?
#include <chrono>
#include <cstdio>
#include <thread>
#include <vector>

long count_above(const std::vector<double>& v, std::size_t lo, std::size_t hi) {
    long n = 0;
    for (std::size_t i = lo; i < hi; ++i) {
        if (v[i] > 25.0) ++n;
    }
    return n;
}

int main(void) {
    std::vector<double> day(1440);              // one sensor, one day
    for (std::size_t i = 0; i < day.size(); ++i) {
        day[i] = 20.0 + static_cast<double>(i % 600) / 60.0;
    }
    using clk = std::chrono::steady_clock;
    long total1 = 0, total2 = 0;
    auto t0 = clk::now();
    for (int rep = 0; rep < 1000; ++rep) {
        total1 += count_above(day, 0, day.size());
    }
    auto t1 = clk::now();
    for (int rep = 0; rep < 1000; ++rep) {
        long a = 0, b = 0;
        std::thread ta([&] { a = count_above(day, 0, 720); });
        std::thread tb([&] { b = count_above(day, 720, 1440); });
        ta.join();
        tb.join();
        total2 += a + b;
    }
    auto t2 = clk::now();
    std::chrono::duration<double, std::micro> one = t1 - t0, two = t2 - t1;
    std::printf("1 thread:  %.2f us per day (total %ld)\n", one.count() / 1000, total1);
    std::printf("2 threads: %.2f us per day (total %ld)\n", two.count() / 1000, total2);
    return 0;
}
ch20/spin_flag.cpp23 lines
// spin_flag.cpp: the writer spins on a plain bool
#include <chrono>
#include <cstdio>
#include <thread>

double reading = 0.0;     // shared, no protection
bool ready = false;       // shared, no protection

int main(void) {
    std::thread sensor([] {
        std::this_thread::sleep_for(
            std::chrono::milliseconds(50));
        reading = 21.75;
        ready = true;                 // "the reading is there"
    });
    std::thread writer([] {
        while (!ready) {}                 // spin until set
        std::printf("writer got %.2f\n", reading);
    });
    sensor.join();
    writer.join();
    return 0;
}
ch20/station.cpp35 lines
// station.cpp: sensor thread and writer thread, fixed buffer
#include <cstdio>
#include <thread>
#include "ring_buffer.h"

const int DAY = 1440;                 // one reading per minute

int main(void) {
    stn::RingBuffer buf(64);
    std::FILE* log = std::fopen("day.csv", "w");
    if (log == nullptr) { std::perror("day.csv"); return 1; }
    std::thread sensor([&] {
        for (int t = 0; t < DAY; ++t) {
            stn::Reading r{1001, t * 60, 20.0 + t % 10, 1};
            while (!buf.push(r)) std::this_thread::yield();
        }
        buf.close();                      // no more readings
    });
    int written = 0;                      // writer thread only
    double sum = 0.0;                     // writer thread only
    std::thread writer([&] {
        stn::Reading r;
        while (buf.pop_wait(r)) {         // sleeps while empty
            std::fprintf(log, "%u,%lld,%.2f\n", r.sensor_id,
                static_cast<long long>(r.time_s), r.value);
            sum += r.value;
            ++written;
        }
    });
    sensor.join();
    writer.join();                    // now main may read
    std::fclose(log);
    std::printf("written=%d sum=%.1f\n", written, sum);
    return 0;
}
ch20/station_agent.cpp29 lines
// station_agent.cpp: the same two threads, agent's buffer
#include <cstdio>
#include <thread>
#include "ring_buffer_agent.h"

const int DAY = 1440;                 // one reading per minute

int main(void) {
    stn::RingBuffer buf(64);
    double sum = 0.0;
    std::thread sensor([&] {
        for (int t = 0; t < DAY; ++t) {
            stn::Reading r{1001, t * 60, 20.0 + t % 10, 1};
            while (!buf.push(r)) std::this_thread::yield();
        }
    });
    std::thread writer([&] {
        int written = 0;
        while (written < DAY) {
            if (buf.empty()) continue;    // unlocked read
            stn::Reading r;
            if (buf.pop(r)) { sum += r.value; ++written; }
        }
    });
    sensor.join();
    writer.join();
    std::printf("sum=%.1f (expected %.1f)\n", sum, DAY * 24.5);
    return 0;
}
ch20/two_stacks.cpp28 lines
// two_stacks.cpp: each thread runs on its own stack
#include <chrono>
#include <cstdio>
#include <thread>

int shared_total = 0;           // one copy, seen by all

void count_to(const char* name, int n) {
    int local = 0;                 // on THIS thread's stack
    for (int i = 0; i < n; ++i) {
        ++local;
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::printf("%s: local=%d at %p, shared_total at %p\n",
                name, local, static_cast<void*>(&local),
                static_cast<void*>(&shared_total));
}

int main(void) {
    int in_main = 7;
    std::printf("main: in_main at %p\n",
                static_cast<void*>(&in_main));
    std::thread t1(count_to, "t1", 3);  // starts running now
    std::thread t2(count_to, "t2", 5);  // runs at the same time
    t1.join();                          // wait for t1 to finish
    t2.join();                          // wait for t2 to finish
    return 0;
}