Multiple Threads
Starting Multiple Threads
// Start 3 threads
std::thread thr1(hello);
std::thread thr2(hello);
std::thread thr3(hello);
// Wait for them to finish
thr1.join();
thr2.join();
thr3.join();
If you have a C++20 compiler, you can use jthread which will call join() internally.
Data Sharing Between Threads
-
The threads in a program share the same memory space
- It is very easy to share data between the threads
-
The only requirement is that the data is visible to the thread functions
- Global or static variable, for global thread functions
- Static class member, for class member thread functions
- Local variable captured by lambda expressions (by reference)
-
Threads interleave their execution
-
Threads can interfere with each other's actions
-
Modifying shared data can cause data corruption
- This is the main source of bugs in concurrent programs
Data Race
- A "Data Race" occurs when:
- Two or more threads access the same memory location
- And at least one of the threads modifies it
- Potentially conflicting accesses to the same memory location
- Only safe if the threads are synchronized
- One thread accesses the memory location at a time
- The other threads have to wait until it is safe for them to access it
- In effect, the threads execute sequentially while they access it
- A data race causes undefined behavior
- The program is not guaranteed to behave consistently
Race Condition
- The outcome is affected by timing changes
- e.g. One client clears a database table
- Another client inserts an entry into the same table
- A data race is a special case of a race condition
- The outcome depends on when threads are scheduled to run
Memory Location
-
In C++, a "memory location" is a scalar object:
- A built-in variable
- A pointer
- An element in a container
-
Or a scalar object: A struct or class member
-
Also an obscure case: A series of contiguous bitfields within the same word, unless they contain a zero-length bitfield
Compound Objects
-
C++ STL containers are memory locations
- Multiple threads modifying the same object may conflict
- Should be synchronized
-
For our own types, we can choose the behavior
- Classes can provide their own synchronization
- Easier to work with
- Calling a sequence of member functions may be problematic
- Usually better to implement them as memory locations