lock the mutex While the predicate is false Wait for the condition (optionally change some data) unlock the mutex.
A mutex that may alter the outcome of the predicate should do something like:
lock the mutex change the data signal the condition unlock the mutex
To demonstrate how this work in practice, we'll reinvent a mutex(!). Our code will make a thread wait until it can achive the lock on the mutex.
// $Id: fakemutex.cpp,v 1.1 2003/08/16 20:45:57 jonnymind Exp $ // fakemutex.cpp - an introductory test // // This program is free software - GPL 2.0 license // (C) Giancarlo Niccolai // // NOTE: this program is demonstrative and hence not compiled // in the make process of Wefts test suite. // #include <wefts.h> using namespace Wefts; class FakeMutex { private: // our condition Condition m_cond; // the mutex related with the condition. Mutex m_mutex; // Status of our mutex. Is it locked? bool m_locked; public: FakeMutex() { // we have to tell the condition what mutex to use. m_cond.setMutex( &m_mutex ); // initally unlocked m_locked = false; } void achive() { m_mutex.lock(); // our predicate: we must wait for the lock to be free while( m_locked ) m_cond.wait(); // this also does m_mutex.unlock() // good news here: m_locked is false AND we hold m_mutex, and // this means we are the only ones that can touch m_locked. m_locked = true; // done, we can release internal mutex m_mutex.unlock(); } void release() { // this ensure we are the only one handling m_locked. m_mutex.lock(); m_locked = false; // hey, we have changed the predicate outcome! m_cond.signal(); // ok, we are done with our data. Lets let it go. m_mutex.unlock(); } };
00001 // $Id: fakemutex.cpp,v 1.1 2003/08/16 20:45:57 jonnymind Exp $ 00002 // fakemutex.cpp - an introductory test 00003 // 00004 // This program is free software - GPL 2.0 license 00005 // (C) Giancarlo Niccolai 00006 // 00007 // NOTE: this program is demonstrative and hence not compiled 00008 // in the make process of Wefts test suite. 00009 // 00010 00011 #include <wefts.h> 00012 using namespace Wefts; 00013 00014 class FakeMutex { 00015 private: 00016 // our condition 00017 Condition m_cond; 00018 00019 // the mutex related with the condition. 00020 Mutex m_mutex; 00021 00022 // Status of our mutex. Is it locked? 00023 bool m_locked; 00024 00025 public: 00026 FakeMutex() { 00027 // we have to tell the condition what mutex to use. 00028 m_cond.setMutex( &m_mutex ); 00029 // initally unlocked 00030 m_locked = false; 00031 } 00032 00033 void achive() { 00034 m_mutex.lock(); 00035 00036 // our predicate: we must wait for the lock to be free 00037 while( m_locked ) 00038 m_cond.wait(); // this also does m_mutex.unlock() 00039 00040 // good news here: m_locked is false AND we hold m_mutex, and 00041 // this means we are the only ones that can touch m_locked. 00042 m_locked = true; 00043 00044 // done, we can release internal mutex 00045 m_mutex.unlock(); 00046 } 00047 00048 void release() { 00049 // this ensure we are the only one handling m_locked. 00050 m_mutex.lock(); 00051 00052 m_locked = false; 00053 00054 // hey, we have changed the predicate outcome! 00055 m_cond.signal(); 00056 00057 // ok, we are done with our data. Lets let it go. 00058 m_mutex.unlock(); 00059 } 00060 };
So, a better pseudocode for the waiter can be:
lock the mutex IF the predicate is false chage some data saying that we are waiting do preparation things While the predicate is false wait undo preparation things change data to state we are not waiting anymore ENDIF work with shared data unlock the mutex.
And the signaler should be just a little smarter:
lock the mutex change the data IF there are threads waiting signal the condition unlock the mutex
To make our mutex reentrant, we must be able to tell if our thread is the one that locked the thread previously. Wefts provides a low-level wrapper class called OSThread that has the support for such tests. We are interested in fuor methods: OSThread::setCurrent() set the internal data of the OSThread object so that it reflects our current thread; OSThread::invalidate() clears those data, so that the object can be considered not referencing any existing thread. OSThread::same() returns true if the thread object matches our current thread, (that is to say, if the caller thread is the same that called OSThread::setCurrent() before), and OSThread::equal( OSThread & ) that returns true if a given OSThread object represent the same system thread as the parameter.
When a thread that has already achieved the lock tries to do it again, a counter is incremented; when it unlocks, the counter is decremented, and when it reaches 0, other threads are allowed trying to get the lock.
Here is the change to our code:
// $Id: rfakemutex.cpp,v 1.1 2003/08/16 20:45:57 jonnymind Exp $ // rfakemutex.cpp - an introductory test // // This program is free software - GPL 2.0 license // (C) Giancarlo Niccolai // // NOTE: this program is demonstrative and hence not compiled // in the make process of Wefts test suite. // #include <wefts.h> using namespace Wefts; class RFakeMutex { private: Condition m_cond; Mutex m_mutex; int m_locked; // notice: now we have a counter here // We also need a count to specify that we are waiting. int m_waiting; //this the thread owning the mutex OSThread m_owner; public: RFakeMutex() { m_cond.setMutex( &m_mutex ); // initial count of locks m_locked = 0; // initial count of threads waiting to achive the lock m_waiting = 0; //the owner is invalidated in its constructor; we don't need to // set it here. } void achive() { m_mutex.lock(); // Now we must see if we must begin our waiting process. if ( m_locked > 0 && ! m_owner.same() ) { // Communicate to the world that there is someone waiting m_waiting++; while( m_locked > 0) m_cond.wait(); // we are not waiting anymore here. m_waiting--; } // ok, if we are here we have the right to achive the lock m_locked++; // and we want to mark the mutex as ours if ( m_locked == 1 ) // first lock request ? m_owner.setCurrent(); m_mutex.unlock(); } void release() { m_mutex.lock(); // are we the rightful owners if ( m_locked > 0 && m_owner.same() ) { m_locked--; // has this changed the predicate outcome? if ( m_locked == 0 ) { m_owner.invalidate(); // we are not the owners anymore //...yes..., we have chaged it, but is there someone interested? if ( m_waiting > 0 ) m_cond.signal(); } } m_mutex.unlock(); } };
00001 // $Id: rfakemutex.cpp,v 1.1 2003/08/16 20:45:57 jonnymind Exp $ 00002 // rfakemutex.cpp - an introductory test 00003 // 00004 // This program is free software - GPL 2.0 license 00005 // (C) Giancarlo Niccolai 00006 // 00007 // NOTE: this program is demonstrative and hence not compiled 00008 // in the make process of Wefts test suite. 00009 // 00010 00011 #include <wefts.h> 00012 using namespace Wefts; 00013 00014 class RFakeMutex { 00015 private: 00016 Condition m_cond; 00017 Mutex m_mutex; 00018 int m_locked; // notice: now we have a counter here 00019 // We also need a count to specify that we are waiting. 00020 int m_waiting; 00021 00022 //this the thread owning the mutex 00023 OSThread m_owner; 00024 00025 public: 00026 RFakeMutex() { 00027 m_cond.setMutex( &m_mutex ); 00028 // initial count of locks 00029 m_locked = 0; 00030 // initial count of threads waiting to achive the lock 00031 m_waiting = 0; 00032 //the owner is invalidated in its constructor; we don't need to 00033 // set it here. 00034 } 00035 00036 void achive() { 00037 m_mutex.lock(); 00038 00039 // Now we must see if we must begin our waiting process. 00040 if ( m_locked > 0 && ! m_owner.same() ) { 00041 00042 // Communicate to the world that there is someone waiting 00043 m_waiting++; 00044 00045 while( m_locked > 0) 00046 m_cond.wait(); 00047 00048 // we are not waiting anymore here. 00049 m_waiting--; 00050 } 00051 // ok, if we are here we have the right to achive the lock 00052 m_locked++; 00053 00054 // and we want to mark the mutex as ours 00055 if ( m_locked == 1 ) // first lock request ? 00056 m_owner.setCurrent(); 00057 00058 m_mutex.unlock(); 00059 } 00060 00061 void release() { 00062 m_mutex.lock(); 00063 00064 // are we the rightful owners 00065 if ( m_locked > 0 && m_owner.same() ) { 00066 m_locked--; 00067 // has this changed the predicate outcome? 00068 if ( m_locked == 0 ) { 00069 m_owner.invalidate(); // we are not the owners anymore 00070 //...yes..., we have chaged it, but is there someone interested? 00071 if ( m_waiting > 0 ) 00072 m_cond.signal(); 00073 } 00074 } 00075 00076 m_mutex.unlock(); 00077 } 00078 };
In this example code, the m_owner.invalidate() in release() method is not strictly necessary, as it is simply overwriten when the lock count gets to 1; anyhow is better to invalidate it as the overhead is minimal, and in the future you may wish to have more sophisticated chech on the current owner of the mutex.
Then change the lines:
while( m_locked > 0) m_cond.wait();
with:
while( m_locked > 0 ) { // second == the parameter you added at achive() decl if( ! m_cond.wait( seconds ) ) { //timed out! m_waiting--; // we are not waiting waiting anymore m_mutex.unlock(); //anyway, we are given our lock. return false; } }
Finally add a return true; after the last line of the method.
You have two options here; you may rely on kind cancelation, using just timed waits and checking periodically if a cancelation request has been sent, or you can use automatized Wefts condition wait cancelation scheme.
If the thread is cancelable when entering a conditon wait, then a stop() call on the waiting thread will cause the condition to be interrupted, and a pre-set routine to be called. Just derive your class from the CleanupHandler, and add a void handleCleanup( int position ) method. Then, use the parameters of wait() to set the cleanup handler that will be called on canelation. You can pass an optional integer parameter that will be given to the method; this allows you to handle different condition wait cleanups from the same handler, if your class or your code needs it.
In our case, the cleanup routine will have to undo all the things the thread has done before entering the wait: the thread has been canceled, and this means that it must release all the claims it has made while waiting for the condition to become true.
This is the final code of our fake mutex:
// $Id: xfakemutex.cpp,v 1.3 2004/03/10 14:57:05 jonnymind Exp $ // xfakemutex.cpp - an introductory test // // This program is free software - GPL 2.0 license // (C) Giancarlo Niccolai // // NOTE: this program is demonstrative and hence not compiled // in the make process of Wefts test suite. // #include <wefts.h> using namespace Wefts; class XFakeMutex: public CleanupHandler { private: Condition m_cond; Mutex m_mutex; int m_locked; int m_waiting; OSThread m_owner; public: XFakeMutex() { m_cond.setMutex( &m_mutex ); m_locked = 0; m_waiting = 0; } void achive() { m_mutex.lock(); if ( m_locked > 0 && ! m_owner.same() ) { m_waiting++; while( m_locked > 0) m_cond.wait( this ); m_waiting--; } m_locked++; if ( m_locked == 1 ) m_owner.setCurrent(); m_mutex.unlock(); } bool tryachive() { m_mutex.lock(); // can we lock? if ( m_locked > 0 && ! m_owner.same() ) { // no? ... well we give up. return false; } // yes? GREAT! m_locked++; if ( m_locked == 1 ) m_owner.setCurrent(); m_mutex.unlock(); return true; } //timed wait bool achive( double seconds ) { m_mutex.lock(); // Now we must see if we must begin our waiting process. if ( m_locked > 0 && ! m_owner.same() ) { m_waiting++; while( m_locked > 0) { if ( ! m_cond.wait( this ) ) { m_waiting--; m_mutex.unlock(); return false; } } m_waiting--; } // ok, if we are here we have the right to achive the lock m_locked++; // and we want to mark the mutex as ours if ( m_locked == 1 ) // first lock request ? m_owner.setCurrent(); m_mutex.unlock(); return true; } void release() { m_mutex.lock(); // are we the rightful owners if ( m_locked > 0 && m_owner.same() ) { m_locked--; // has this changed the predicate outcome? if ( m_locked == 0 ) { m_owner.invalidate(); // we are not the owners anymore //...yes..., we have chaged it, but is there someone interested? if ( m_waiting > 0 ) m_cond.signal(); } } m_mutex.unlock(); } // we don't need the parameter here... void handleCleanup( int ) { // undoing all the preparations m_waiting --; // no more waiting m_mutex.unlock(); // releasing the mutex we are given on entrance. } };
00001 // $Id: xfakemutex.cpp,v 1.3 2004/03/10 14:57:05 jonnymind Exp $ 00002 // xfakemutex.cpp - an introductory test 00003 // 00004 // This program is free software - GPL 2.0 license 00005 // (C) Giancarlo Niccolai 00006 // 00007 // NOTE: this program is demonstrative and hence not compiled 00008 // in the make process of Wefts test suite. 00009 // 00010 00011 #include <wefts.h> 00012 using namespace Wefts; 00013 00014 class XFakeMutex: public CleanupHandler { 00015 private: 00016 Condition m_cond; 00017 Mutex m_mutex; 00018 int m_locked; 00019 int m_waiting; 00020 OSThread m_owner; 00021 00022 public: 00023 XFakeMutex() { 00024 m_cond.setMutex( &m_mutex ); 00025 m_locked = 0; 00026 m_waiting = 0; 00027 } 00028 00029 void achive() { 00030 m_mutex.lock(); 00031 00032 if ( m_locked > 0 && ! m_owner.same() ) { 00033 m_waiting++; 00034 00035 while( m_locked > 0) 00036 m_cond.wait( this ); 00037 00038 m_waiting--; 00039 } 00040 m_locked++; 00041 00042 if ( m_locked == 1 ) 00043 m_owner.setCurrent(); 00044 00045 m_mutex.unlock(); 00046 } 00047 00048 bool tryachive() { 00049 m_mutex.lock(); 00050 00051 // can we lock? 00052 if ( m_locked > 0 && ! m_owner.same() ) { 00053 // no? ... well we give up. 00054 return false; 00055 } 00056 // yes? GREAT! 00057 m_locked++; 00058 00059 if ( m_locked == 1 ) 00060 m_owner.setCurrent(); 00061 00062 m_mutex.unlock(); 00063 return true; 00064 } 00065 00066 00067 //timed wait 00068 bool achive( double seconds ) { 00069 m_mutex.lock(); 00070 00071 // Now we must see if we must begin our waiting process. 00072 if ( m_locked > 0 && ! m_owner.same() ) { 00073 m_waiting++; 00074 00075 while( m_locked > 0) { 00076 if ( ! m_cond.wait( this ) ) { 00077 m_waiting--; 00078 m_mutex.unlock(); 00079 return false; 00080 } 00081 } 00082 00083 m_waiting--; 00084 } 00085 // ok, if we are here we have the right to achive the lock 00086 m_locked++; 00087 00088 // and we want to mark the mutex as ours 00089 if ( m_locked == 1 ) // first lock request ? 00090 m_owner.setCurrent(); 00091 00092 m_mutex.unlock(); 00093 return true; 00094 } 00095 00096 00097 void release() { 00098 m_mutex.lock(); 00099 00100 // are we the rightful owners 00101 if ( m_locked > 0 && m_owner.same() ) { 00102 m_locked--; 00103 // has this changed the predicate outcome? 00104 if ( m_locked == 0 ) { 00105 m_owner.invalidate(); // we are not the owners anymore 00106 //...yes..., we have chaged it, but is there someone interested? 00107 if ( m_waiting > 0 ) 00108 m_cond.signal(); 00109 } 00110 } 00111 00112 m_mutex.unlock(); 00113 } 00114 00115 // we don't need the parameter here... 00116 void handleCleanup( int ) 00117 { 00118 // undoing all the preparations 00119 m_waiting --; // no more waiting 00120 m_mutex.unlock(); // releasing the mutex we are given on entrance. 00121 } 00122 };
As you can see, our fake mutex is not anymore so fake; it implements reentrancy, timed trylocks and cancelation during lock achiving. In fact, this is a modified code taken from Wefts::XMutex
Two basic classes are provided: FastCondition and RCondition. The first is implemented with a flat non-reentrant (and thus fast) mutex, while the second is derived from a reentrant mutex and a condition. So, if your program does not need mutex reentrancy (as a well designed application should), you can safely use the FastCondition version, while you can use RCondition if you plan to use recursive locks.
To understand how a FastCondition may work, get the XFakeMutex class and:
Voila', you are done: more code readability and more incapsulation.
The best aspect of this class is the fact that you may want to add the data related with the condition predicate to a subclass of your own, like that:
class MyCondition: public FastCondition, public cleanup { public: int m_waiting; int m_locked; MyCondition() :FastCondition() { m_waiting = 0; m_locked = 0; } ... };
This encapsulates all the data that can be shared among threads in one object, which has already synchronization primitives both for excluding access from shared data AND for waiting for the data to satisfy a predicate. If you will, you can even code the predicate as a member of your class... The advantage of this is that you need to send a just pointer around to move a whole lot of data, the method assiciated with that and the syncrhonization needed to handle that correcly across threads.
Well complex mutexes as XMutex, RWMutex and RRWMutex are not descendants of the mutex class; a read-write mutex cannot be used to lock the predicate that a condition must wait for. To explain why, I need just one word: mess. Using a complex mutex to wait for a condition rises too many problems, as, "Should we use a read-only lock or a read write one?" or "should we starve readers or writers?" not to talk about cancelation issues. What would be the cleanup sequence? Would you cancel the wait or the mutex lock? Too messy, really.
But then, it can be still interesting to provide timed, interruptable, read only/readwrite access to the data you may also want to wait for changes.
The answer is to implement a mix of a conditon and a complex mutex like the one we need.
Observe this class
class MyThing: public RRWMutex { void * data; ... }; class Pulsar: public CleanupHandler { ... MyThing *cargo; bool m_changed; FastCondition m_cond; ... void WaitForAChange() { m_cond.lock(); while( ! m_changed ) m_cond.wait(this, 0); cond.unlock(); // IMPORTANT: do it after. cargo->lockRead(); // or the kind of lock you need. } };
Done. When there is something worth the attention of a waiter, other threads working on the data will just change the m_changed and signal the condition. The only drawback is that anything could happen before WaitForAChange() can achive its lock(), but this is probably unimportant, as nothing is gonna change the fact that m_changed told us that we must take care of something in the cargo that has changed.
This is the most common example of deadlock.
If you need precisely to serve some data when a change happens in another thread, and you need not to have interferences while waiting to be able to get the locks, try the Subscription class.
This can be handled also with a single condition variable, but doing so would wake up, in some moments, threads whose predicate is known not to be changed, and causing in this way useless wakeups of threads, that is to say, useless context switches.
A typical example is a set of reader and writer threads operating on a finite ring buffer( See Wefts::RingBuffer ).
Using just one condition, if the buffer becomes full and the writers are put in wait, a reader would signal to every thread that it has freed a cell; obviously, all the other readers may be uninterested in this.
00001 // $Id: smartring.cpp,v 1.3 2004/03/10 14:57:05 jonnymind Exp $ 00002 // smartring.cpp 00003 // 00004 // This program is free software - GPL 2.0 license 00005 // (C) Giancarlo Niccolai 00006 // 00007 // NOTE: this program is demonstrative and hence not compiled 00008 // in the make process of Wefts test suite. 00009 // 00010 00011 #include <wefts.h> 00012 using namespace Wefts; 00013 00014 // an arbitrary small size 00015 #define BUFDIM 10 00016 00017 class SmartRing: public CleanupHandler 00018 { 00019 // the mutex 00020 Mutex m_mutex; 00021 00022 // reader's condition 00023 Condition m_rCond; 00024 00025 // writer's condition 00026 Condition m_wCond; 00027 00028 int m_buffer[ BUFDIM ]; // a buffer 00029 // write pointer 00030 int m_wPos; 00031 // read pointer 00032 int m_rPos; 00033 00034 // writers waiting 00035 int m_wWaiting; 00036 // readers waiting 00037 int m_rWaiting; 00038 00039 public: 00040 SmartRing() 00041 { 00042 m_wPos = 1; // read + 1 == write means empty 00043 m_rPos = 0; 00044 00045 // no thread waiting. 00046 m_wWaiting = 0; 00047 m_rWaiting = 0; 00048 00049 // setting here our mutex to work with two conditions 00050 m_wCond.setMutex( &m_mutex ); 00051 m_rCond.setMutex( &m_mutex ); 00052 00053 } 00054 00055 // is our ring empty? 00056 bool empty() { 00057 return (m_wPos == m_rPos + 1|| (m_wPos == 0 && m_rPos == BUFDIM-1)); 00058 } 00059 00060 // is our ring full? 00061 bool full() { 00062 return (m_wPos == m_rPos); 00063 } 00064 00065 void write( int data ) 00066 { 00067 m_mutex.lock(); 00068 00069 // have we to wait? 00070 if( full() ) { 00071 m_wWaiting++; 00072 while ( full() ) { 00073 // on cleanup, use this as cleanup handler and 0 as position 00074 m_wCond.wait( this, 0 ); 00075 } 00076 m_wWaiting--; 00077 } 00078 00079 // ok, we can write; 00080 // we take note of the other predicate to signal if it changes 00081 bool was_empty = empty(); 00082 00083 // write then advance 00084 m_buffer[ m_wPos ++ ] = data; 00085 00086 // ring-around 00087 if ( m_wPos == BUFDIM ) m_wPos = 0; 00088 00089 //now, if the predicate changed, and if someone is listening, signal. 00090 if( was_empty && m_rWaiting != 0 ) { 00091 m_rCond.signal(); // tell the readers situation has changed. 00092 } 00093 00094 // done 00095 m_mutex.unlock(); 00096 } 00097 00098 int read() 00099 { 00100 m_mutex.lock(); 00101 00102 // have we to wait? 00103 if( empty() ) { 00104 m_rWaiting++; 00105 while ( empty() ) { 00106 // on cleanup, use this as cleanup handler and 1 as position 00107 m_rCond.wait( this, 1 ); 00108 } 00109 m_rWaiting--; 00110 } 00111 00112 // ok, we can read 00113 // we take note of the other predicate to signal if it changes 00114 bool was_full = full(); 00115 00116 // advance and then read 00117 if ( m_rPos == BUFDIM ) 00118 m_rPos = 0; 00119 else 00120 m_rPos++; 00121 00122 int data = m_buffer[ m_rPos ]; 00123 00124 //now, if the predicate changed, and if someone is listening, signal. 00125 if( was_full && m_wWaiting != 0 ) { 00126 m_wCond.signal(); // tell the writers situation has changed. 00127 } 00128 00129 // done 00130 m_mutex.unlock(); 00131 return data; 00132 } 00133 00134 void handleCleanup( int pos ) 00135 { 00136 if ( pos == 0 ) 00137 // cleanup a writer 00138 m_wWaiting--; 00139 else 00140 // clean up a reader 00141 m_rWaiting--; 00142 00143 m_mutex.unlock(); 00144 } 00145 };
As you can see from lines 50 and 51, you can assign a mutex to any given amount of conditions. Moreover, when a condition is destroyed, its mutex remains intact, so it is possible to dynamically allocate and destroy conditions while holding a pivotal mutex that is assigned to all of them.
As you can see in lines 91 and 126, using two conditions is possible to have more focused signalations, an thus more efficient code; the cost of an additional condition is really small (as Condition is a very small class), while if we used just one condition, we would have had to signal threads that would have probably not been interested.