you have a race. What happens is you check available and see none, so you step in to *1 ; then the
producer runs, publishes whatever and signals - but there are no waiters yet so the signal is lost.
Then you go into the wait() and deadlock. This is the "lost wakeup" problem.

So, the double check avoids this race. What must the semantics of prepare_wait & wait be for it to work?
It's something like this :

Any signal that happens between "prepare_wait" and "wait" must cause "wait" to not block (either because
the waitable handle is signalled, or through some other mechanism).

Some implementations of a prepare_wait/wait mechanism may have spurious signals; eg. wait might not block even though you shouldn't
really have gotten a signal; because of that you will usually loop in the consumer.

Now let's look at a few specific solutions to this problem :

condition variables

This is the locking solution to the race. It doesn't use double-checked wait, instead it uses a mutex to protect the race; the
naive producer/consumer is replaced with :

for instance, "waitset" could be a vector of handles with a mutex protecting access to that vector. This would be a race without the prepare_wait
and double checking.

In this case we ensure the double-checked semantics works because the current thread is actually added to the waitset in prepare_wait. So
any signal that happens before we get into wait() will set our Event, and our wait() will not actually block us, because the event is already set.

eventcount

Thomasson's eventcount accomplishes the same thing but in a different way. A simplified version of it works like this :

in this case, prepare_wait doesn't actually add you to the waitset, so signals don't go to you, but it still works, because if signal was called
in the gap, the count will increase and no longer match your key, so you will not do the wait.

That is, it specifically detects the race - it sees "was there a signal between when I did prepare_wait and wait?" , and if so, it doesn't
go into the wait.
The consumer should loop, so you keep trying to enter the wait until you get to check your condition without a signal firing.

futex

It just occurred to me yesterday that futex is actually another solution to this exact same problem. You may recall - futex does an internal
check of your pointer against a value, and only goes into the wait if the value matches.

we see can clearly see that futex is just double-checked-wait in disguise.

That is, futex is really our beloved prepare_wait -> wait pattern , but only for the case that the wait condition
is of the form *ptr == something.

Do we like the futex API? Not really. I mean it's nice that the OS provides it, but if you are designing your
own waitset you would never make the API like that. It confines you to only working on single ints, and
your condition has to be int == value. A two-call API like "prepare_wait / wait" is much more flexible,
it lets you check conditions like "is this lockfree queue empty" which are impossible to do with futex (what
you wind up doing is just doing the double-check yourself and use futex just as an "Event", either that or
duplicating the condition into an int for futex's benefit (but that is risky, it can race if not done right,
so not recommended)).