The classic waitset pattern :
check condition
waiter w;
waitset.prepare_wait(&w);
double check condition
w.wait();
waitset.retire_wait(&w);
lends itself very easily to setting a waiter flag. All you do is change the double check into a CAS that
sets that flag. For example say your condition is count > 0 , you do :
if ( (count&0x7FFFFFFF) == 0 )
{
waiter w;
waitset.prepare_wait(&w);
// double check condition :
int c = count.fetch_or( 0x80000000 ); // set waiter flag and double check
if ( (c&0x7FFFFFFF) == 0 )
w.wait();
waitset.retire_wait(&w);
}
then in notify, you can avoid signalling when the waiter flag is not set :
// publish :
int c = count.atomic_inc_and_mask(1,0x7FFFFFFF);
// notify about my publication if there were waiters :
if ( c & 0x80000000 )
waitset.notify();
(note : don't be misled by using count here; this is still not a good way to build a semaphore; I'm
just using an int count as a simple way of modeling a publish/consume.
I was being obtuse before when I wrote about the problems with waitset OR. It is important to
be aware of those issues when working with waitsets, because they are inherent to how waitsets work
and you will encounter them in some form or other, but of course you can do an OR if you extend
the basic waitset a little.
What you do is give waiter an atomic bool to know if it's been signalled, something like :
struct waiter
{
atomicbool> signalled;
os_handle waitable_handle;
}
(a "waiter" is a helper which is how you add your "self" to the waitset; depending on the waitset
implementation, waitable_handle might be your thread ID for example).
Then in the waitset notify you just do :
if ( w->signalled.exchange(true) == false )
{
Signal( w->waitable_handle );
}
else
step to next waiter in waitset and try him again.
That is, you try to only send the signal to handles that need it.
If we use this in the simple OR example from a few days ago, then both waiting threads will wake up -
two notify_ones will wake two waiters.
While you're at it, your waiter struct may as well also contain the origin of the signal, like :
if ( w->signalled.exchange(true) == false )
{
// non-atomic assignment :
w->signal_origin = this; // this is a waitset
Signal( w->waitable_handle );
}
That way when you wake from an OR wait you know why.
(note that I'm assuming your os_handle only ever does one state transition - it goes from unsignalled
to signalled. This is the correct way to use waitset; each waiter() gets a new waitable handle for
its lifetime, and it only lives for the length of one wait. In practice you actually recycle the waiters
to avoid creating new ones all the time, but you recycle them safely in a way that you know they cannot
be still in use by any thread (alternatively you could just have a waiter per thread in its TLS and reset
them between uses))
(BTW of course you don't actually use atomic bool in real code because bool is too badly defined)
"11-30-11 - Some more Waitset notes"
No comments yet. -