A helper template function to wait for WaitOnAddress in a loop

The Wait­On­Address function suffers from the problem of spurious wake-ups. This means that most uses of the Wait­On­Address function are of the form "while the value is bad, wait for it to change."

There is a subtlety here, because you have to capture the value, then make your decision based on the captured value, and if you decide that you want to wait some more, you need to pass the captured value to Wait­On­Address. The extra capturing is necessary to avoid a race condition if you determine that the value is bad, but before you can call Wait­On­Address, the value becomes good.

Here's a simple helper function to encapsulate the loop:

template<typename T, typename TLambda>
void WaitForValueByAddress(T& value, TLambda&& is_okay)
  auto capturedValue = value;
  while (!is_okay(capturedValue)) {
    WaitOnAddress(&value, &capturedValue, sizeof(value), INFINITE);
    capturedValue = value;

The assumption here is that T is a simple value type like int32_t. If you pass a funky class, then we're going to be copying it, which is probably a bad idea given that the variable is going to be asynchronously modified (possibly while we are copying it).

The predicate evaluates the value: Return true if it acceptable, or return false to reject it and wait some more.

Here's a sample usage:

int32_t someValue;

void WaitForValueToBecomeZero()
  WaitForValueByAddress(someValue, [](auto&& v) { return v == 0; });
Comments (6)
  1. kantos says:

    If you want to be uber safe about how this is called you can use a std::enable_if_t<std::is_invocable_r_v<bool, TLambda(T&&), T&&>, int> = 0 as a third template parameter to disable this method if the caller passes a non-invokable type. and then call is_okay using std::invoke. This check will insure that whatever is passed in returns a bool and that the input type matches the type of T.

    1. Peter Doubleday says:

      In practice, you’d have a bit of a problem doing that. Not sure if that is C++11 or C++14, but regrettably most C++ shops haven’t even progressed that far. (In my limited experience.)

      1. Peter Doubleday says:

        Then again, the example uses auto&&, so perhaps I’m just quibbling about the std library call.

      2. kantos says:

        It’s C++17 but you can do something equivalently in C++11 it’s just longer and more annoying, you’d have to do result_of checks and is_function but they will overly restrict what you can call. But honestly this code uses a universal reference anyway so it’s at least C++11 and lambdas with auto is C++14 IIRC.

        That said those stuck on ancient compilers have my condolences.

  2. Alex Cohn says:

    Spurious wakeups are hard. Until very recently, WebRTC had timeout wait using relative timestamp like this (on some Android devices):

    while (!event_status_ && error == 0) {
    error = pthread_cond_timedwait_relative_np(
    &event_cond_, &event_mutex_, &ts);

    After spurious wakeup, it went to wait for the same amount of milliseconds again.

  3. Jon says:

    Isn’t it possible that breaks due to compiler optimizations?

    Suppose that is_okay() is moderately complicated. The compiler decides that is_okay() is either simple enough to inline – this is especially likely to happen if is_okay() is only used in this one place, which is fairly likely. Clearly is_okay() doesn’t change “value”, so let’s assume the compiler can prove that. Then, since we’ve assumed is_okay() is moderately complicated, consider what happens if the compiler runs out of registers during is_okay(), and it has to spill something. There’s an obvious solution there – it spills “capturedValue”. Since it knows “capturedValue” was read from memory, and it is allowed to assume that memory doesn’t change, it can just re-read “value” after is_okay() and before calling WaitOnAddress(), and the optimizer thinks this is good because it saves a write to the stack. Now, if the value becomes okay after “value” is read the first time and checked with is_okay() but before it’s read the second time for the WaitOnAddress() call, you end up passing an okay value to WaitOnAddress() and blocking forever.

    The fix is to make sure the type of “value” is marked”volatile”, e,.g. “volatile int32_t”, so the compiler knows it’s not allowed to read twice when you’ve only written one read. Or alternatively, use a special atomic read function to read “value”, that has appropriate compiler-specific magic to ensure it doesn’t get optimized into multiple reads.

    (I think the above is unlikely, because is_okay is likely to be simple enough that it won’t run out of registers. But I’d rather be certain, because it’s not going to be fun to debug that very intermittent hang in several year’s time, shortly after upgrading to an improved compiler).

Comments are closed.

Skip to main content