A very interesting article. But the real punchline is the very last sentence:
> I wonder why there is even a returned status at all if you cannot rely on it.
With a normal untimed condition variable, we are taught to check for spurious wakeups. So not:
// Bad
Mutex.lock();
if (!itemReady) {
Cond.wait();
}
DoStuff();
But instead:
// Good
Mutex.lock();
while (!itemReady) {
Cond.wait();
}
DoStuff();
And so it goes with a timed condition variable. The return value is irrelevant. Instead, manually inspect the situation:
* Is the condition you're waiting for satisfied e.g. with queue non-empty? If so, great, use it! (There's a tiny chance of race condition here, where the timer expired after the item was issued on the queue but before the condition variable was signalled. But that absolutely doesn't matter.)
* Otherwise, recheck the time since you started waiting. Is the time now greater than the deadline? If so, you've timed out.
* Otherwise, it was a spurious wakeup. Go back to sleep with the same deadline (i.e. shorter wait period) as before.
If you do that in your code then the race described in the article won't affect you.
> The wait condition is associated with a mutex, which is locked by the user when calling wake() and that is also passed locked to wait(). The implementation is supposed to unlock and wait atomically.
In POSIX it is not necessary to lock the mutex before calling wake(). In fact it is better unlock the mutex first, and then call wake(), because otherwise the just-woken thread will contend with the waker. Given that fact, the race really is unavoidable.
A few years ago I strongly defended this very point on here HN. As far as I knew, you only need to signal while owning the lock if you want to rely on the stronger guarantees of realtime scheduling.
Since then I stumbled upon another scenario were you need to hold the lock for correctness: a producer calls pthread_cond_signal which wakes up a waiter that then destroys the condition variable. This would get flagged by tsan and often cause segfaults especially when the memory is freed. It was not obvious from the docs that this is not allowed, but as apparently pthread_cond_signal can still inspect the condition variable states after waking a waiter.
My use case was fairly simple (I was implementing an event count), so I reimplemented it on top of a raw futex and made sure that signal would return straight away, but signaling while holding the mutex and only destroying the condvar after releasing the mutex would have also fixed it.
More seriously, I'd love to see this modeled in Promela/Spin. There was a go bug a while back where I reproduced it in TLA+ and someone else reproduced it in Spin, and I personally liked the Spin solution a lot more. It seems well-suited for these kinds of problems!
> I wonder why there is even a returned status at all if you cannot rely on it.
With a normal untimed condition variable, we are taught to check for spurious wakeups. So not:
But instead: And so it goes with a timed condition variable. The return value is irrelevant. Instead, manually inspect the situation:* Is the condition you're waiting for satisfied e.g. with queue non-empty? If so, great, use it! (There's a tiny chance of race condition here, where the timer expired after the item was issued on the queue but before the condition variable was signalled. But that absolutely doesn't matter.)
* Otherwise, recheck the time since you started waiting. Is the time now greater than the deadline? If so, you've timed out.
* Otherwise, it was a spurious wakeup. Go back to sleep with the same deadline (i.e. shorter wait period) as before.
If you do that in your code then the race described in the article won't affect you.