Understanding `select or delay until` in tasks

I’m playing with tasks and have a question about select with delay until.

I have a task with the following body (the full implementation is on GitHub):

Wait : loop
   select
      -- Wait for pings. When they come, reset Last_Ping.
      accept Ping do
          Log.Debug ("Received ping.");
          Last_Ping := Clock;
      end Ping;
   or
      -- If there is no ping within the expected interval, do something.
      delay until (Last_Ping + Expected_Ping_Interval);
      Log.Error ("Timeout!");
      -- Do something.
      -- Is there any way to keep running the task loop here? 
   end select;
end loop Wait;

From how I understand the select, it waits for the specified delay - if there is a Ping before the end of the delay, the loop iteration is finished and the next iteration is started, again waiting for the specified delay or accepting a Ping.

Now, when the delay is over and there was no entry in Ping, it does not proceed with the next loop iteration but is finished, right? Why is that? And is there a way to write this loop so the task continues running?

(Also happily take pointers to good resources for learning about task behavior in more depth. :slight_smile:)

1 Like

It should continue to run, and your example here will do that.
There are two ways to get your described behaviour.

One is an exception is raised, but not caught (so always add an exception handler to your tasks, or add a last chance handler).

The other is a deadlock.

Your full implementation calls a callback, Reboot_Watched_Application.all, pointing to Reboot, which calls Start_Processing, which calls Watchdog.Ping.
This means you have a task trying to call itself.
It may or may not be caught by the runtime, so it either just deadlocks, or it raises an exception (probably Tasking_Error or Program_Error)

Ah, thanks! :bulb:

It looks like it deadlocks. But it seems to do that also if I split it into two tasks, one timer to keep track of the pings and delay, and one that calls the reboot.

Btw, what would be a good resource to read about concurrency? Maybe “Concurrent and Real-Time Programming in Ada” by Burns & Welling? I feel like my mental model of how things work is not only very fuzzy but also mostly wrong.

That’s a good option. My personal favourite for the whole language (not just tasking), is " Programming in Ada" by John Barnes. The current version is upddated with a preview of Ada 2022 features.

Btw, deadlocks should be “visible” in the stack traces of your tasks.
if on Linux, try pstack <pid> or run thread apply all backtrace when attached to your program in gdb

1 Like

delay until (Last_Ping + Expected_Ping_Interval); is an absolute delay, not a relative delay. It uses Last_Ping as a time reference.
I can see 2 problems in your example :

  • Last_Ping is not set before entering the loop.
  • Once the timeout has occurred, Last_Ping is not updated so the delay until statement is non blocking making the loop entering the or branch permanently (when there is no Ping) potentially hanging your application by consuming all CPU cycles.

Thanks everyone for helping out! This is gold.
I’m now off for a vacation, but will dive deeper into tasking afterwards.