Threads
Condition
There might be situations where 2 threads need to synchronize and coordinate with each other. It's possible that the first thread does something and then waits. The second thread does something and then signals the first thread. The first thread receives the signal and then resumes. This is where the "condition_variable" comes in. It works hand in hand with the mutex, unique_lock and lock_guard classes.File: cond1.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> using namespace std ; mutex mutexObject ; condition_variable cv; bool ready = false; bool processed = false; void worker_thread() { cout << "Worker thread starting.\n"; // Wait until main() sends signal unique_lock lk( mutexObject ); //ready = true ; //It has locked the lock. Wait needs the lock to be //locked. this_thread::sleep_for(chrono::seconds(5)); //wait releases the lock. Once we get a notify we //will acquire the lock again if it is free. cv.wait(lk, []{ return ready; }); // after the wait, we own the lock. cout << "Worker thread is processing after wait.\n"; cout << "Worker thread signals dtr1 processing completed\n"; // Manual unlocking is done before notifying, to avoid waking up // the waiting thread only to block again (see notify_one for details) lk.unlock(); processed = true; cv.notify_one(); } int main() { //worker thread starts and waits for a signal from the //main thread thread worker(worker_thread); { //Let the thread lock first //this_thread::sleep_for(chrono::seconds(5)); lock_guard lk(mutexObject); cout << "main() signals worker thread ready to start.\n"; //Is ready enough to unblock worker thread ? // //this_thread::sleep_for(chrono::seconds(10)); ready = true; cv.notify_one(); //The worker thread cannot proceed at this stage //because the lock has not been freed. After the next //lines the lock guard will go out of scope //this_thread::sleep_for(chrono::seconds(30)); } //cv.notify_one(); // wait for the worker { cout << "Inside main() waiting for the worker thread. Step 1 \n"; unique_lock lk(mutexObject); cout << "Inside main() waiting for the worker thread. Step 2 \n"; //this_thread::sleep_for(chrono::seconds(5)); cv.wait(lk, []{ return processed; }); } cout << "Back in main()" << '\n'; worker.join(); } $ rm a.exe ; g++ cond1.cpp ; ./a.exe main() signals worker thread ready to start. Worker thread starting. Inside main() waiting for the worker thread. Step 1 Worker thread is processing after wait. Worker thread signals dtr1 processing completed Inside main() waiting for the worker thread. Step 2 Back in main()
The above program is written in a way that allows us to experiment
and learn about the difference concepts with conditions.
The flow is as:
main thread worker thread
thread worker(worker_thread);
Starts the worker sleeps for 5 seconds
thread: Waits for signal from the main thread.
cv.wait(lk, []{ return ready; });
Signals the worker
thread:
cv.notify_one(); worker thread wakes up
prints
cout << "Worker thread is processing after wait.\n";
waits for a signal from
the child thread
cv.wait(lk, []{ return processed; });
Worker thread signals
the main thread.
processed = true;
cv.notify_one();
Main thread waits for worker thread
and exits.
We have the following declarations:
mutex mutexObject ;
condition_variable cv;
bool ready = false;
bool processed = false;
The worker thread is started and waits for a signal.
The "wait" method needs a lock . We lock the mutex
using the syntax:
unique_lock lk( mutexObject );
Then we pass the lock to the "wait" method.
cv.wait(lk, []{ return ready; });
There are 2 variants of the "wait" method. Here we
are using the one that takes a predicate. What this
is saying is that the predicate has to be true and a
signal received then the wait will proceed. The "wait"
method also releases the lock on the mutex. This makes
sense because another thread might want to lock the
same mutex.
The main thread executes:
lock_guard lk(mutexObject);
cout << "main() signals worker thread ready to start.\n";
//Is ready enough to unblock worker thread ?
//
//this_thread::sleep_for(chrono::seconds(10));
ready = true;
cv.notify_one();
We are using a "lock_guard" on the same mutex. We print something
and set "ready" to true and then call "notify_one()". However
the worker thread will not wake up just yet. We have not released
the lock held by the "lock_guard". Once we pass the block then
and the lock guard object goes out scope then, the worker thread
wakes up.
The main thread now waits for the worker thread to finish and signal
the main thread.
cout << "Inside main() waiting for the worker thread. Step 1 \n";
unique_lock lk(mutexObject);
cout << "Inside main() waiting for the worker thread. Step 2 \n";
//this_thread::sleep_for(chrono::seconds(5));
cv.wait(lk, []{ return processed; });
The main thread waits and we are using the predicate again.
The documentation for "wait" states that it is a good idea to use
"predicate" so that we dan "ignore spurious awakenings" . We need
a lock in order to call the "wait" method and we use "unique_lock" for
that. Let's examine the output:
$ rm a.exe ; g++ cond1.cpp ; ./a.exe
1) main() signals worker thread ready to start.
2) Worker thread starting.
3) Inside main() waiting for the worker thread. Step 1
4) Worker thread is processing after wait.
5) Worker thread signals dtr1 processing completed
6) Inside main() waiting for the worker thread. Step 2
7) Back in main()
We have labelled the output statements so that we can refer
to them. The main program prints "1)" and starts the "worker
thread" and the main program executes:
lock_guard lk(mutexObject);
This will lock the global object. That means when the
worker thread encounters:
lock_guard lk(mutexObject);
It is going to "hang" because the "locK()" method
will encounter the mutex object that was locked by the
main thread. How can we validate this ?
We will modify our code a bit.
File: cond2.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> using namespace std ; mutex mutexObject ; condition_variable cv; bool ready = false; bool processed = false; void worker_thread() { cout << "Worker thread starting.\n"; // Wait until main() sends signal unique_lock lk( mutexObject ); cout << "worker thread about to acquire the lock.\n" ; //ready = true ; //It has locked the lock. Wait needs the lock to be //locked. this_thread::sleep_for(chrono::seconds(5)); //wait releases the lock. Once we get a notify we //will acquire the lock again if it is free. cv.wait(lk, []{ return ready; }); // after the wait, we own the lock. cout << "Worker thread is processing after wait.\n"; cout << "Worker thread signals dtr1 processing completed\n"; // Manual unlocking is done before notifying, to avoid waking up // the waiting thread only to block again (see notify_one for details) lk.unlock(); processed = true; cv.notify_one(); } int main() { //worker thread starts and waits for a signal from the //main thread thread worker(worker_thread); { //Let the thread lock first //this_thread::sleep_for(chrono::seconds(5)); lock_guard lk(mutexObject); cout << "main() signals worker thread ready to start.\n"; //Is ready enough to unblock worker thread ? // //this_thread::sleep_for(chrono::seconds(10)); ready = true; cv.notify_one(); //The worker thread cannot proceed at this stage //because the lock has not been freed. After the next //lines the lock guard will go out of scope this_thread::sleep_for(chrono::seconds(30)); cout << "main() about to release the lock guard.\n" ; } //cv.notify_one(); // wait for the worker { cout << "Inside main() waiting for the worker thread. Step 1 \n"; unique_lock lk(mutexObject); cout << "Inside main() waiting for the worker thread. Step 2 \n"; //this_thread::sleep_for(chrono::seconds(5)); cv.wait(lk, []{ return processed; }); } cout << "Back in main()" << '\n'; worker.join(); } $ rm a.exe ; g++ cond2.cpp ; ./a.exe main() signals worker thread ready to start. Worker thread starting. main() about to release the lock guard. worker thread about to acquire the lock. Inside main() waiting for the worker thread. Step 1 Worker thread is processing after wait. Worker thread signals dtr1 processing completed Inside main() waiting for the worker thread. Step 2 Back in main() Notice the delay after the line Worker thread starting. This is coming from the delay in the lock getting released by the main thread in line: this_thread::sleep_for(chrono::seconds(30)); cout << "main() about to release the lock guard.\n" ; } Only after the main releases the lock do we see the worker thread acquire the lock. main() about to release the lock guard. Inside main() waiting for the worker thread. Step 1 worker thread about to acquire the lock.
File: deadlock.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> using namespace std ; mutex mutexObject ; condition_variable cv; bool ready = false; bool processed = false; void worker_thread() { cout << "Worker thread starting.\n"; // Wait until main() sends signal unique_lock lk( mutexObject ); //ready = true ; //It has locked the lock. Wait needs the lock to be //locked. this_thread::sleep_for(chrono::seconds(5)); //wait releases the lock. Once we get a notify we //will acquire the lock again if it is free. cv.wait(lk, []{ return ready; }); // after the wait, we own the lock. cout << "Worker thread is processing after wait.\n"; cout << "Worker thread signals dtr1 processing completed\n"; // Manual unlocking is done before notifying, to avoid waking up // the waiting thread only to block again (see notify_one for details) lk.unlock(); processed = true; cv.notify_one(); } int main() { //worker thread starts and waits for a signal from the //main thread thread worker(worker_thread); { //Let the thread lock first //this_thread::sleep_for(chrono::seconds(5)); lock_guard lk(mutexObject); cout << "main() signals worker thread ready to start.\n"; //Is ready enough to unblock worker thread ? // //this_thread::sleep_for(chrono::seconds(10)); //ready = true; cv.notify_one(); //The worker thread cannot proceed at this stage //because the lock has not been freed. After the next //lines the lock guard will go out of scope //this_thread::sleep_for(chrono::seconds(30)); } //cv.notify_one(); // wait for the worker { cout << "Inside main() waiting for the worker thread. Step 1 \n"; unique_lock lk(mutexObject); cout << "Inside main() waiting for the worker thread. Step 2 \n"; //this_thread::sleep_for(chrono::seconds(5)); cv.wait(lk, []{ return processed; }); } cout << "Back in main()" << '\n'; worker.join(); } $ rm a.exe ; g++ deadlock.cpp ; ./a.exe main() signals worker thread ready to start. Worker thread starting. Inside main() waiting for the worker thread. Step 1 Inside main() waiting for the worker thread. Step 2 The above code is same as "cond1.cpp" except that we have taken out the statement : //ready = true; The worker thread is waiting for the signal from the main thread. The main thread sends the signal but since the "ready" is still set to false; the wait in the worker thread does not proceed. The main thread proceeds and hit's it's own wait statement. Now we have 2 threads that cannot proceed any further leading to a deadlock. Using threads in a program introduces it's own challenges and we must be careful that our understanding of thread concepts and our program is complete.
Buffer Reader/Writer problem.This is the classic problem where we have a buffer and threads that are writing and reading the buffer. There are 2 types of threads: writer and reader. If the reader thread tries to read a value from the buffer and the buffer is empty then the reader thread waits till something is written to the buffer. If the writer thread tries to write something to the buffer and the buffer is full then the writer thread waits till something is read from the buffer. If the reader thread encountered an empty buffer and the writer thread wrote something to it then it should notify the reader thread to go ahead and read.
File: reader_writer.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> #include <unistd.h> using namespace std ; std::mutex mutexObject1 ; std::mutex mutexObject2 ; std::mutex mtxG ; // mutex for critical section mutex mtxW ; class buffer { public: int* holder ; int counter ; int length_of_buffer ; std::condition_variable cv1 ; std::condition_variable cv2 ; buffer( int buffersize ) { cout << "Buffer costructor with size:" + buffersize << endl ; counter = -1 ; //counter is empty length_of_buffer = buffersize ; //if counter == length_of_buffer-1 means full holder = new int[buffersize] ; } int read( int threadId ) { int retValue = 0 ; //cout << "Start of bufferObject:read() 1: " << endl ; //lockObject1.lock(); std::unique_lock lockObject1(mutexObject1) ; //cout << "Start of bufferObject:read() 2: " << endl ; try { //check if empty while ( counter == -1 ) { cout << "bufferObject:read(): " << " Waiting for something to be written. threadId :" << threadId << " \r\n" ; cv1.wait( lockObject1 ); } //Need to protect this section mtxG.lock() ; int ret ; retValue = holder[counter] ; counter-- ; mtxG.unlock() ; //if ( counter == -1 ) // ready1 = false ; cout <<"Read value:" << retValue << " by thread:" << threadId << " counter" << counter << "\r\n" ; //lockObject2.unlock(); cv2.notify_one(); } catch(exception except ) { cout << "except.toString()" << endl ; } return retValue ; } //-------------------------------------------------------------------------------------- void write( int val1 , int threadId) { std::unique_lock lockObject2( mutexObject2 ) ; try { //check if full while ( counter == (length_of_buffer-1) ) { cout << "bufferObject:write(): Buffer full...Waiting to be read.:" << "Thread.currentThread() threadId :" << threadId << "\r\n" ; //cv2.wait(lockObject2, []{ return ready2; }); cv2.wait( lockObject2 ); } mtxG.lock() ; counter++ ; holder[counter] = val1 ; mtxG.unlock() ; cout <<"Adding value:" << val1 << " by thread:" << threadId << " Thread.currentThread().getName() counter:" << counter << endl ; cv1.notify_one(); lockObject2.unlock() ; } catch( exception e ) { cout << e.what() << endl ; } } } ; //----------------------------------------------------------- class writer { public: buffer& bufferObject ; int threadId ; int interval ; static int value ; writer( buffer& bufferObjectP, int intervalP , int threadIdP ) : bufferObject ( bufferObjectP ), threadId( threadIdP ) { interval = intervalP ; } void run() { try { while ( true ) { mtxW.lock() ; bufferObject.write( value , threadId ) ; value++ ; mtxW.unlock() ; sleep( interval ) ; } } catch( exception e ) { cout << "e.toString()" << endl ; } } } ; int writer::value = 1; class reader { public: buffer& bufferObject ; int interval ; int threadId ; reader( buffer& bufferObjectP, int intervalP, int threadIdP ) : bufferObject ( bufferObjectP ) , threadId( threadIdP ) { interval = intervalP ; } void run() { try { int value ; while ( true ) { value = bufferObject.read( threadId ) ; sleep( interval ) ; } } catch( exception e ) { cout << "read e.toString()" << endl ; } } }; //---------------------------------------------------- int main() { buffer bufferObject( 2 ) ; writer writerObject( bufferObject , 10 , 1 ) ; writer writerObject1( bufferObject , 3 , 2 ) ; reader readerObject( bufferObject , 5 , 1 ) ; reader readerObject1( bufferObject , 5 , 2 ) ; std::thread worker1 ( &writer::run, &writerObject ); std::thread worker4 ( &writer::run, &writerObject1 ); std::thread worker2 ( &reader::run, &readerObject ); std::thread worker3 ( &reader::run, &readerObject1 ); worker1.join() ; worker2.join() ; worker3.join() ; worker4.join() ; return 0 ; } $ rm a.exe ; g++ reader_writer.cpp ; ./a.exe ffer costructor with size: Adding value:1 by thread:1 Thread.currentThread().getName() counter:0 Adding value:2 by thread:2 Thread.currentThread().getName() counter:1 Read value:2 by thread:1 counter0 Read value:1 by thread:2 counter-1 Adding value:3 by thread:2 Thread.currentThread().getName() counter:0 Read value:3 by thread:2 counter-1 bufferObject:read(): Waiting for something to be written. threadId :1 Adding value:4 by thread:2 Thread.currentThread().getName() counter:0 Read value:4 by thread:1 counter-1
Exercise
File: mutex_ex_1.cpp
Solutions
1)File: mutex_ex_1s.cpp
2)
File: mutex_ex_2s.cpp
2)
File: guard_ex_1s.cpp