Threads and atomics
لم يُترجم بعد؛ يُعرض باللغة الإنجليزية
A thread is one path through your program, and the threads of a program share its memory. Start two threads that each add 1 to a shared counter 100000 times, and watch why a plain int can come out too low: ++ is really a load, an add and a store, so two threads can lose an update. That is a data race. Then make the counter a std::atomic<int>, wait for the threads with join(), and get 200000 every time.
5 أسئلة سريعة
اختر إجابة لتعرف السبب. تُحفَظ نتيجتك مع تقدّمك.
01What do the threads of one program share?
All threads of a program see the same memory, so a global such as hits is shared. Each thread has its own stack, so local variables like the loop counter i are its own.
02Two threads each run ++hits once on a plain int that holds 5. Which results are possible?
++hits is a load, an add and a store. If one thread finishes before the other loads, the result is 7. If both load 5 before either stores, both store 6 and one update is lost. (Because this is a data race, C++ itself promises nothing at all.)
03What is a data race in C++?
That is the definition. A program with a data race has undefined behaviour: the standard sets no rules for what it does.
04Why does std::atomic<int> hits{0}; fix the counter?
Atomic read-modify-writes on one variable happen one at a time, each reading the latest value. No update is lost, so the total is 200000 every run.
05What happens if a std::thread is destroyed while it is still joinable (you never called join)?
std::thread's destructor calls std::terminate if the thread is joinable. Call join() (or use C++20's std::jthread, which joins by itself).
المسرد
- Thread
- One path of execution through a program: it runs one statement after another, with its own stack. A program can run several at once.
- Main thread
- The thread every program starts with. It runs main().
- join()
- A std::thread member function that waits until that thread has finished.
- Register
- A tiny storage slot inside a CPU core. Arithmetic such as adding 1 happens in registers, not directly in memory.
- Data race
- Two threads access the same memory location, at least one writes, and nothing orders the accesses. In C++ this is undefined behaviour.
- Atomic
- Indivisible: an atomic operation happens all at once, so no other thread can see it half done. std::atomic<int> makes ++ atomic.
- Read-modify-write
- An operation that reads a value, changes it and writes it back, such as ++. On an atomic it is one indivisible step.
- Undefined behaviour
- A program the C++ standard sets no rules for. It may seem to work, give wrong answers or crash.
الملاحظات والمصادر
- The card leaves out the program's first three lines, #include <atomic>, #include <iostream>, #include <thread>. The full program compiles with g++ or clang++ using -std=c++20 -Wall -Wextra and prints 200000.
- -pthread tells GCC and Clang to use the system's POSIX threads library. Recent Linux and macOS systems often build without it, but older Linux systems need it, so it is safest to always add it. Microsoft's compiler needs no extra flag.
- Real compilers may keep hits in a register for a while, or merge many additions into one when you turn on optimisation. It may, because for a plain int it is allowed to assume no other thread uses hits at the same time. That is one more reason the racy total is unpredictable.
- Undefined behaviour means the C++ standard sets no rules for what the program does. A racy counter usually just loses updates, but the compiler is free to assume races never happen, so you cannot rely on any result.
- ++hits on a std::atomic<int> is the same as hits.fetch_add(1) + 1. By default atomics use the strictest ordering, std::memory_order_seq_cst. A plain counter like this one could use the cheaper std::memory_order_relaxed, but the default is the safe choice to start with.
- How much faster the local-count version runs depends on the machine. Sharing one atomic between cores is slow because each core must get the latest copy of hits from the others (Williams calls it cache ping-pong).
- cppreference.com: std::thread
- cppreference.com: std::thread::join
- cppreference.com: std::thread::~thread
- cppreference.com: std::jthread
- cppreference.com: Memory model (threads and data races)
- cppreference.com: std::atomic
- cppreference.com: std::atomic<T>::fetch_add
- cppreference.com: std::memory_order
- Standard C++ Foundation (isocpp.org): C++11 Standard Library Extensions: Concurrency
- Standard C++ Foundation (isocpp.org): C++11 Language Extensions: Concurrency
- GCC manual: Options for Linking (-pthread)
- Bryant and O'Hallaron, Computer Systems: A Programmer's Perspective, 3rd ed., §12.5 (CMU)
- Anthony Williams, C++ Concurrency in Action, 2nd ed. (Manning, 2019)
اختبار يتاح بعد الخطوة الأخيرة.