SDL_WaitConditionTimeout — problem with the example code in the docs

There is an example code on the SDL_WaitConditionTimeout function documentation page, its main loop looks like this:

while (!done) {
    SDL_LockMutex(lock);
    while (!condition && SDL_WaitConditionTimeout(cond, lock, timeout) == 0) {
        continue;
    }
    SDL_UnlockMutex(lock);
    if (condition) {
        /* ... */
    }
    /* ... do some periodic work */
}

This (waiting) loop:

while (!condition && SDL_WaitConditionTimeout(cond, lock, timeout) == 0) {
    continue;
}

is invalid — it will end only if a condition variable is signaled; or the condition flag is set to true, or the SDL_WaitConditionTimeout will return true. So this loop will behave the same as the SDL_WaitCondition, just will wait forever.

I did some test (in Free Pascal) and to fix the problem and actually take the timeout into account, it must look like this:

while (!done) {
    SDL_LockMutex(lock);

    while (!condition) {
        if (SDL_WaitConditionTimeout(cond, lock, timeout) == 0)
            break;
    }
    SDL_UnlockMutex(lock);

    if (condition) {
        /* ... */
    }

    /* ... do some periodic work */
}

However, I have a problem understanding why a logical flag (the condition variable) is used at all to wait for a condition variable (type SDL_Condition) to be signaled, and why the SDL_WaitCondition and SDL_WaitConditionTimeout functions are called in a loop.

Can somebody explain me all of this mysteries and how to properly write a loop that waits for the signal and takes the timeout into account?

Here is a simple demo that demonstrates the fix:

uses
  SDL3;

var
  Condition:  PSDL_Condition = nil;
  Mutex:      PSDL_Mutex     = nil;
  Thread:     PSDL_Thread    = nil;
  Flag:       Boolean        = False;
  Terminated: Boolean        = False;

  function ThreadWorker (AData: Pointer): Integer; cdecl;
  var
    Signaled: Boolean;
  begin
    repeat
      SDL_LockMutex(Mutex);

      // This loop seems to do waiting in the right way.
      while not Flag do
        if not SDL_WaitConditionTimeout(Condition, Mutex, 500) then
          break;

      Signaled := Flag;
      Flag     := False;
      SDL_UnlockMutex(Mutex);

      if not Terminated then
      begin
        if Signaled then
          WriteLn('  ', SDL_GetTicks():5, ' Extra work.');

        WriteLn(SDL_GetTicks():5, ' Periodic work.');
      end;
    until Terminated;

    Result := 0;
  end;

label
  CleanUp;
var
  Window: PSDL_Window;
  Event:  TSDL_Event;
begin
  SDL_Init(SDL_INIT_EVENTS);

  Window    := SDL_CreateWindow('', 320, 240, 0);
  Thread    := SDL_CreateThreadRuntime(@ThreadWorker, nil, nil, nil, nil);
  Condition := SDL_CreateCondition();
  Mutex     := SDL_CreateMutex();
  Flag      := False;

  while True do
  begin
    while SDL_PollEvent(@Event) do
    case Event._Type of

      SDL_EVENT_KEY_DOWN:
      case Event.Key.Scancode of
        SDL_SCANCODE_SPACE:
        begin
          SDL_LockMutex(Mutex);
          Flag := True;
          SDL_SignalCondition(Condition);
          SDL_UnlockMutex(Mutex);
        end;

        SDL_SCANCODE_ESCAPE: goto CleanUp;
      end;

      SDL_EVENT_QUIT: goto CleanUp;
    end;

    SDL_Delay(10);
  end;

CleanUp:
  Terminated := True;

  SDL_LockMutex(Mutex);
  Flag := True;
  SDL_SignalCondition(Condition);
  SDL_UnlockMutex(Mutex);
  SDL_WaitThread(Thread, nil);

  SDL_DestroyCondition (Condition);
  SDL_DestroyMutex     (Mutex);
  SDL_DestroyWindow    (Window);
  SDL_Quit();
end.

In short, the worker thread prints a message in the console every 500ms, but if you press the Space, it will wake up before the timeout is reached and print extra message in the console. Sample output:

 608 Periodic work.
 1109 Periodic work.
 1609 Periodic work.
 2109 Periodic work.
 2609 Periodic work.
 3110 Periodic work.
   3277 Extra work.
 3277 Periodic work.
 3777 Periodic work.
 4278 Periodic work.
 4779 Periodic work.
 5279 Periodic work.
 5779 Periodic work.
 6280 Periodic work.
 6780 Periodic work.
 7280 Periodic work.
   7350 Extra work.
 7350 Periodic work.
 7850 Periodic work.
 8351 Periodic work.
   8513 Extra work.
 8513 Periodic work.
 9014 Periodic work.
 9515 Periodic work.
10015 Periodic work.
10516 Periodic work.
{...}

While not directly spelled out, the documentation sample looks like a standardr worker thread. It waits until it’s told to do some work, using a condition variable and SDL_WaitConditionTimeout() to put the thread to sleep instead of busy waiting.

As to your “fix”:

while(!condition) {
    if(SDL_WaitConditionTimeout(cond, lock, timeout) == 0)
        break;
}

This does not loop until the condition variable has been signaled. SDL_WaitConditionTimeout() returns false (0) if the timeout has been reached, and in your “fix” it then exits the wait loop and goes about doing whatever work it was supposed to be waiting to do.

The flag is there to keep from exiting the wait loop in case of a spurious wake up.

1 Like

On the other hand, if you wanted to wait until the condition variable had been signaled OR the timeout had been reached, that’s when you’d do something like what you posted

bool timed_out = false;
...
while(!condition) {
    if(SDL_WaitConditionTimeout(cond, lock, timeout) == false) {
        timed_out = true;
        break;
    }
}

if(timed_out) {
    // handle timeout
} else {
    //
}
1 Like

Ok, now everything is clear. I suspected that the thread might wake up too early and hence the loop to put it to sleep again. And yes, that’s what I mean, to recognize if a variable is signaled or a timeout has been reached, so your proposal is accurate. Thank you!

But that’s a bit misleading, since this example doesn’t really show a sensible use of the SDL_WaitConditionTimeout function, since the timeout is not taken into account at all, and the thread waits forever for the condition variable to be signaled.

IMO it would be a good idea to change this example to show how to use this function and check if the timeout has been reached. Otherwise this example makes no sense, because it does exactly the same thing as SDL_WaitCondition.

Another tip: avoid WriteLn, this is not thread-fixed. Use SDL_Log instead, it is persistent.
In your example, WriteLn works, but as soon as you use SDL_CreateThreadRuntime twice, it fails with a “runtime error 101”.

      if not Terminated then
      begin
        if Signaled then
          SDL_Log('  %5d Extra work.', SDL_GetTicks);

        SDL_Log('%5d Periodic work.',  SDL_GetTicks);
      end;

WriteLn is thread safe. Here is confirmation of this, from the compiler developer himself:

PascalDragon in this topic:

It is on all platforms that support threads as the corresponding file variables are declared as threadvar. What is not guaranteed (on any platform) is the order of the output and that you might not get mixed output.

I use WriteLn mainly because it is easy and convenient to use, it is part of the RTL (SDL is not needed), and it prints what I want, without any prefixes or filtering.

I use Windows 10 and have never received this error, even with dozens of threads printing data to the console, as fast as possible. So I don’t know where you got this error from, especially since runtime error 101 is about the disk being full and unable to write data:

101 Disk write error
Reported when the disk is full, and you’re trying to write to it.

I use Linux and it crashes. I also noticed that 101 means disk full. I also managed to get rid of this error with Linux Pure and multithreading.
I put this in and the error went away.

pthread_mutex_lock(@mutex);
Write(Thread.index, ' ');
pthread_mutex_unlock(@mutex);

Somehow FPC interprets a write conflict as a disk full.

I’ve never tried how it reacts with Windows.