Threads
Contents
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: condition1.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> using namespace std ; mutex mutexObject ; condition_variable cv; bool ready1 = false ; bool ready2 = false ; void worker_thread1() { cout << "worker_thread1() ...entering.\n"; ready1 = false ; unique_lock lk( mutexObject ); cv.wait(lk, []{ return ready1; }); cout << "worker_thread1() doing some work.\n"; this_thread::sleep_for(chrono::seconds(1)); cout << "worker_thread1() exiting.\n"; } void worker_thread2() { cout << "worker_thread2() ...entering.\n"; ready2 = false ; unique_lock lk( mutexObject ); cv.wait(lk, []{ return ready2; }); cout << "worker_thread2() doing some work.\n" ; this_thread::sleep_for(chrono::seconds(1)) ; cout << "worker_thread2() exiting.\n" ; } int main() { thread worker1(worker_thread1); thread worker2(worker_thread2); cout << "main thread() waiting for 5 seconds.\n" ; this_thread::sleep_for(chrono::seconds(5)) ; cout << "main thread() sending signal to worker thread 1.\n" ; ready1 = true ; cv.notify_one() ; cout << "main thread() waiting for 5 seconds.\n" ; this_thread::sleep_for(chrono::seconds(5)) ; cout << "main thread() sending signal to worker thread 2.\n" ; ready2 = true ; cv.notify_one() ; worker1.join(); worker2.join(); } $ g++ condition1.cpp ; ./a.exe worker_thread1() ...entering. main thread() waiting for 5 seconds. worker_thread2() ...entering. main thread() sending signal to worker thread 1. main thread() waiting for 5 seconds. worker_thread1() doing some work. worker_thread1() exiting. main thread() sending signal to worker thread 2. worker_thread2() doing some work. worker_thread2() exiting.We have a main thread and 2 worker threads. The 2 worker threads wait. The main thread waits for 5 seconds and starts worker thread 1 and then waits for another 5 seconds and starts worker thread2 .
The"wait" calls essentially goes into a wait state. The thread must have had the lock before calling "wait" and the mutex must be wrapped in a "unique_lock" . Another thread can do a "notify_one" on the condition variable associated with the lock to wake the waiting thread up. The waiting thead will reacquire the lock and proceed. Note that we do not need to hold the lock in order to call "notify_one". Also if there are no threads waiting then nothing happens if "notify_one" is called.
We modify our program so that the main thread signals the worker thread1 and then waits for the worker thread1 to signal it back once the worker thread1 is done. However there is an issue.Check the exercise section.
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.Some of the things to consider are: 1) Can 2 read threads execute at the same time ? 2) Can a read and write thread execute at the same time ? 3) Does wait need a boolean variable in this case ? 4) Can we have 1 mutex and 2 conditional variables ?
File: reader_writer.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> #include <unistd.h> using namespace std ; class buffer { public: int* holder ; int counter ; int length_of_buffer ; mutex mutexObject1 ; mutex mutexObject2 ; mutex mtxG ; condition_variable cv1 ; 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 // if count == -1 the buffer is empty holder = new int[buffersize] ; } int read( int threadId ) { int retValue = 0 ; //cout << "Start of bufferObject:read() 1: " << endl ; unique_lock lockObject1(mutexObject1) ; 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 //Another writer thread may try to modify the //counter at the same time. mtxG.lock() ; retValue = holder[counter] ; counter-- ; mtxG.unlock() ; cout <<"Read value:" << retValue << " by thread:" << threadId << " counter" << counter << "\r\n" ; // Do we need //lockObject2.unlock(); cv2.notify_one(); } catch( exception except ) { cout << "except.toString()" << endl ; } return retValue ; } //-------------------------------------------------------------------------------------- void write( int val1 , int threadId) { 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(); } catch( exception e ) { cout << e.what() << endl ; } } } ; //----------------------------------------------------------- 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 ) ; this_thread::sleep_for(chrono::seconds(interval)) ; } } catch( exception e ) { cout << "read e.toString()" << endl ; } } }; //----------------------------------------------------------- class writer { public: buffer& bufferObject ; int threadId ; int interval ; static mutex mtxW ; 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() ; this_thread::sleep_for(chrono::seconds(interval)) ; } } catch( exception e ) { cout << "e.toString()" << endl ; } } } ; //---------------------------------------------------- int writer::value = 1; mutex writer::mtxW ; //---------------------------------------------------- int main() { buffer bufferObject( 2 ) ; writer writerObject( bufferObject , 1 , 1 ) ; writer writerObject1( bufferObject , 1 , 2 ) ; reader readerObject( bufferObject , 10 , 3 ) ; reader readerObject1( bufferObject , 10 , 4 ) ; thread worker1 ( &writer::run, &writerObject ); thread worker2 ( &writer::run, &writerObject1 ); thread worker3 ( &reader::run, &readerObject ); thread worker4 ( &reader::run, &readerObject1 ); worker1.join() ; worker2.join() ; worker3.join() ; worker4.join() ; return 0 ; } $ rm a.exe ; g++ reader_writer.cpp ; ./a.exe Buffer 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-1The below code uses a single mutex with 2 conditional variables.
File: reader_writer_1.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> #include <unistd.h> using namespace std ; class buffer { public: int* holder ; int counter ; int length_of_buffer ; mutex mutexObject1 ; mutex mtxG ; condition_variable cv1 ; 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 ; 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" ; //lockObject1.unlock(); cv2.notify_one(); this_thread::sleep_for(chrono::seconds(1)) ; } catch(exception except ) { cout << "except.toString()" << endl ; } return retValue ; } //-------------------------------------------------------------------------------------- void write( int val1 , int threadId) { unique_lock lockObject2( mutexObject1 ) ; 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 ; //lockObject2.unlock(); cv1.notify_one(); this_thread::sleep_for(chrono::seconds(1)) ; } catch( exception e ) { cout << e.what() << endl ; } } } ; //----------------------------------------------------------- class writer { public: buffer& bufferObject ; int threadId ; int interval ; static mutex mtxW ; 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; mutex writer::mtxW ; 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 , 1 , 1 ) ; writer writerObject1( bufferObject , 1 , 2 ) ; reader readerObject( bufferObject , 10 , 3 ) ; reader readerObject1( bufferObject , 10 , 4 ) ; thread worker1 ( &writer::run, &writerObject ); thread worker4 ( &writer::run, &writerObject1 ); thread worker2 ( &reader::run, &readerObject ); thread worker3 ( &reader::run, &readerObject1 ); worker1.join() ; worker2.join() ; worker3.join() ; worker4.join() ; return 0 ; } 1) Can the reader and writer threads enter at the same time ? And if not then do we really need "mtxG". 2) The reader thread does "cv2.notify_one();" but at this time the single mutex is locked.Do we really need "lockObject1.unlock();" ? Note if a lock is not free then a wait will not proceed even if it receives the notification. The next example explores this. The below program gets rid of mtxG and also unlocks the object before doing a "notify_one" .
File: reader_writer_2.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> #include <unistd.h> using namespace std ; class buffer { public: int* holder ; int counter ; int length_of_buffer ; mutex mutexObject1 ; mutex mtxG ; condition_variable cv1 ; 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 ; 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] ; this_thread::sleep_for(chrono::seconds(1)) ; counter-- ; // mtxG.unlock() ; //if ( counter == -1 ) // ready1 = false ; cout <<"Read value:" << retValue << " by thread:" << threadId << " counter" << counter << "\r\n" ; lockObject1.unlock(); cv2.notify_one(); this_thread::sleep_for(chrono::seconds(1)) ; } catch(exception except ) { cout << "except.toString()" << endl ; } return retValue ; } //-------------------------------------------------------------------------------------- void write( int val1 , int threadId) { unique_lock lockObject2( mutexObject1 ) ; 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 ; lockObject2.unlock(); cv1.notify_one(); this_thread::sleep_for(chrono::seconds(1)) ; } catch( exception e ) { cout << e.what() << endl ; } } } ; //----------------------------------------------------------- class writer { public: buffer& bufferObject ; int threadId ; int interval ; static mutex mtxW ; 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; mutex writer::mtxW ; 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 , 1 , 1 ) ; writer writerObject1( bufferObject , 1 , 2 ) ; reader readerObject( bufferObject , 10 , 3 ) ; reader readerObject1( bufferObject , 10 , 4 ) ; thread worker1 ( &writer::run, &writerObject ); thread worker4 ( &writer::run, &writerObject1 ); thread worker2 ( &reader::run, &readerObject ); thread worker3 ( &reader::run, &readerObject1 ); worker1.join() ; worker2.join() ; worker3.join() ; worker4.join() ; return 0 ; }Exercise
1) The below program has 2 threads.
One thread will print an even number and the other thread will print an odd number. The threads alternate and one thread will not print 2 numbers consecutively. Analyse the program and discuss the 2 approaches.
File: condition1_ex.cpp
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> using namespace std ; mutex mutexObj1 ; mutex mutexObj2 ; mutex mutexObj3 ; condition_variable cv; mutex mutexG ; bool var1 = false ; bool var2 = false ; void function3() { for( int i1= 0 ; i1 < 10 ; i1 += 2 ) { unique_lock lk( mutexObj3 ) ; var1 = false ; cout << "i1:" << i1 << endl ; var2 = true ; cv.notify_one(); cv.wait(lk, []{ return var1; }); } //for cout << "Out of the for loop for function 3." << endl ; var2 = true ; cv.notify_one(); } void function4() { for( int i2= 1 ; i2 < 10 ; i2 += 2 ) { unique_lock lk( mutexObj3 ); var2 = false ; cout << "i2--:" << i2 << endl ; var1 = true ; cv.notify_one(); cv.wait(lk, []{ return var2; }); this_thread::sleep_for( chrono::microseconds(1) ); } //for var1 = true ; cv.notify_one(); cout << "Out of the for loop for function 4." << endl ; } void function1() { for( int i1= 0 ; i1 < 10 ; i1 += 2 ) { mutexObj1.lock() ; mutexG.lock() ; cout << "i1:" << i1 << endl ; mutexG.unlock() ; mutexObj1.unlock() ; } } void function2() { for( int i2= 1 ; i2 < 10 ; i2 += 2 ) { mutexObj1.lock() ; mutexG.lock() ; cout << "i2:" << i2 << endl ; mutexG.unlock() ; mutexObj1.unlock() ; //this_thread::sleep_for( chrono::microseconds(1) ); } //for } int main() { //mutexObj2.lock() ; //this_thread::sleep_for( chrono::seconds(2) ); thread th1 ( function1 ) ; thread th2 ( function2 ) ; // thread th1 ( function3 ) ; // thread th2 ( function4 ) ; //this_thread::sleep_for( chrono::seconds(5) ); //mutexObj1.unlock() ; mutexObj2.unlock() ; th1.join() ; th2.join() ; return 0 ; }2) Compile and run the below program. There are some issues when running. Analyse the program and fix it.
File: condition2_ex.cpp
#include <condition_variable> #include <iostream> #include <mutex> #include <string> #include <thread> using namespace std ; mutex mutexObject ; condition_variable cv; bool ready1 = false ; bool ready2 = false ; bool readyMain = false ; void worker_thread1() { cout << "worker_thread1() ...entering.\n"; ready1 = false ; unique_lock lk( mutexObject ); cv.wait(lk, []{ return ready1; }); cout << "worker_thread1() doing some work.\n"; this_thread::sleep_for(chrono::seconds(10)); readyMain = true ; cv.notify_one() ; cout << "worker_thread1() exiting.\n"; } void worker_thread2() { cout << "worker_thread2() ...entering.\n"; ready2 = false ; unique_lock lk( mutexObject ); cout << "worker_thread2() grabbed the lock\n"; cv.wait(lk, []{ return ready2; }); cout << "worker_thread2() doing some work.\n" ; this_thread::sleep_for(chrono::seconds(1)) ; cout << "worker_thread2() exiting.\n" ; } int main() { thread worker1(worker_thread1); thread worker2(worker_thread2); cout << "main thread() waiting for 5 seconds.\n" ; this_thread::sleep_for(chrono::seconds(5)) ; cout << "main thread() sending signal to worker thread 1.\n" ; ready1 = true ; cv.notify_one() ; cout << "main thread() before unique_lock.\n" ; unique_lock lk( mutexObject ); cout << "main thread() waiting for worker thread 1.\n" ; cv.wait(lk, []{ return readyMain; }); cout << "main thread() after waiting for worker thread 1.\n" ; cout << "main thread() waiting for 5 seconds.\n" ; this_thread::sleep_for(chrono::seconds(5)) ; cout << "main thread() sending signal to worker thread 2.\n" ; ready2 = true ; cv.notify_one() ; worker1.join(); worker2.join(); }2) Compile and run the below program. Analyse the program.
File: reader_writer_ex1.cpp
Solutions
2)
File: condition2s_ex.cpp