C++ Multithreading Tutorial
C++ Multithreading Tutorial
net/multithreading_concepts Home
References
1. Sutter, H. & Laurus, J. (2005). Software and the Concurrency Revolution. ACM Queue.
Note. You can skip the 'Multi-threading vs. multi-processing' section if you know the difference and don't care about the details.
Multi-threading vs. multi-processing Race Conditions Mutexes Deadlock Livelock Condition Variables
Further reading
Schmidt, D., Huston, S. (2001). C++ Network Programming. Volume 1. (Chapter 5). Addison-Wesley. Concepts and Synchronisation PrimitivesupRace Conditions http://paulbridger.net/node/17 http://paulbridger.net/race_conditions
Race Conditions
Submitted by pbridger on Mon, 2006-02-27 02:10. A race condition is where the behaviour of code depends on the interleaving of multiple threads. This is perhaps the most fundamental problem with multi-threaded programming. When analysing or writing single-threaded code we only have to think about the sequence of statements right in front of us; we can assume that data will not magically change between statements. However, with improperly written multi-threaded code non-local data can change unexpectedly due to the actions of another thread. Race conditions can result in a high-level logical fault in your program, or (more excitingly) it may even pierce C++'s statement-level abstraction. That is, we cannot even assume that single C++ statements execute atomically because they may compile to multiple assembly instructions. In short, this means that we cannot guarantee the outcome of a statement such as foo += 1; if foo is non-local and may be accessed from multiple threads. A contrived example follows. Listing 1. A logical race condition
int sharedCounter = 50; void* workerThread(void*) { while(sharedCounter > 0) { doSomeWork(); --sharedCounter; } }
Now imagine that we start a number of threads, all executing workerThread(). If we have just one thread, doSomeWork() is going to be executed the correct number of times (whatever sharedCounter starts out at). However, with more than one thread doSomeWork() will most likely be executed too many times. Exactly how many times depends on the number of threads spawned, computer architecture, operating system scheduling and...chance. The problem arises because we do not test and update sharedCounter as an atomic operation, so there is a period where the value of sharedCounter is incorrect. During this time other threads can pass the test when they really shouldn't have. The value of sharedCounter on exit tells us how many extra times doSomeWork() is called. With a single thread, the final value of sharedCounter is of course 0. With multiple threads running, it will be between 0 and -N where N is the number of threads. Moving the update adjacent to the test will not make these two operations atomic. The window during which sharedCounter is out of date will be smaller, but the race condition remains. An illustration of this non-solution follows: Listing 2. Still a race condition
void* workerThread(void*) { while(sharedCounter > 0) { --sharedCounter; doSomeWork(); }
The solution is to use a mutex to synchronise the threads with respect to the test and update. Another way of saying this is that we need to define a critical section in which we both test and update the sharedCounter. The next section introduces mutexes and solves the example race condition. Multi-threading vs. multi-processingupMutexes
Mutexes
Submitted by pbridger on Mon, 2006-02-27 02:09.
Mutexes
A mutex is an OS-level synchronisation primitive that can be used to ensure a section of code can only be executed by one thread at a time.
It has two states: locked and unlocked. When the mutex is locked, any further attempt to lock it will block (the calling thread will be suspended). When the mutex becomes unlocked, if there are threads waiting one of these will be resumed and will lock the mutex. Furthermore, the mutex may only be unlocked by the thread that locked it. If we have a resource we need to share between threads, we associate a mutex with it and use the mutex to synchronise resource access. All we need to do is ensure our code locks the mutex before using the resource, and unlocks it after it is finished. This will prevent race conditions related to multiple threads simultaneously accessing that resource. Diagram 1. Two thread contention for a mutex
int sharedCounter = 50; boost::mutex counterMutex; void solutionWorkerThread() { while(sharedCounter > 0) { bool doWork = false; { // scoped_lock locks mutex boost::mutex::scoped_lock lock(counterMutex); if(sharedCounter > 0) { --sharedCounter; doWork = true; } // scoped_lock unlocks mutex automatically at end of scope } if(doWork) doSomeWork(); } }
In the above solution, the shared counter is checked and updated as an atomic operation (with respect to multiple threads) so the race condition is solved. Note the way the scoped_lock works: the constructor locks the associated mutex and the destructor unlocks it. This is the RAII (Resource Acquisition Is Initialisation) idiom [1], and it helps exception safety. If an exception were thrown while we had locked the mutex, the scoped_lock would be destroyed during the normal stack unwinding process and the mutex would be automatically freed. Exception safety is not an issue with this simple example, since no statement can throw while we have the mutex locked. However, real-world code will almost always benefit from the scoped_lock design. Unfortunately concurrent code can have many problems: race conditions are only the most fundamental. The next problem we'll cover is called Deadlock, and it commonly arises from the interaction of blocking mutexes.
References
1. Stroustrup, B. (1997). The C++ Programming Language. Adsison-Wesley. Race ConditionsupDeadlock