Timeouts and timers
Waiting with a time limit, sleeping and starting a task later all go through the core's timer
thread. It is off until you call EnableTimers. This guide shows the waits with a
timeout on the single-waiter primitives, SleepTask, tasks started after a delay,
and how such a start is cancelled or moved to now. The examples are small pieces of
guide-timeouts.cpp, a complete program that uses them and checks the results.
Enabling timers
if (!TC::EnableTimers()) // starts the core's timer thread (counted)
return 1;EnableTimers starts the timer thread, which starts timed tasks, wakes sleeping
tasks and ends waits that time out. It returns false only if the thread could not be started.
Calls are counted, like EnableAsyncIo: a library and the application
can both enable timers, and only the last DisableTimers stops the thread.
Important
Without EnableTimers, a wait with a timeout waits without a limit, SleepTask
returns false at once, and starting a task by time fails (returns false).
Durations
All times are a Duration, counted in units of 100 ns, the unit of Windows timers. Build one with Duration::Milliseconds, Duration::Microseconds or Duration::Seconds, so a plain number never passes for a duration. Duration::Infinite means no limit: waiting with it is waiting without a timeout.
A duration is a minimum. The timer thread waits on a high-resolution waitable timer: on Windows 10 version 1803 and later it fires on a grid of about 0.5 ms, on older systems on the system tick of about 15.6 ms. A task may start or wake that much later than requested, and later still if all scheduler threads are busy with other tasks.
Waiting with a timeout
Only the single-waiter variants accept a timeout: Semaphore_s,
AutoResetEvent_s, ManualResetEvent_s,
TaskGroup_s, WaitableTask_s and the waitable I/O requests.
Their Wait(Duration timeout) returns true if the wait was satisfied and false if it timed out:
// Waits until the settings are loaded, but at most 200 ms.
static bool
WaitForSettings(TC::Synchronization::ManualResetEvent_s& loaded) noexcept
{
if (loaded.Wait(TC::Duration::Milliseconds(200)))
return true; // signaled in time
std::printf("loading the settings takes too long\n");
return false; // timed out: nothing was taken, a Set may still come
}A signal that comes in time ends the wait early. When the wait times out, nothing is taken (no permit, no signal) and the task no longer waits on the object.
Important
After a timed wait returned false, the other side may still signal the object later. Destroy an
_s event only when no Set can come any more. The same applies to I/O requests: a timeout of
Wait on a request does not cancel the operation (see Asynchronous files).
CriticalSection and CriticalSection_s have no timeouts. The multi-waiter variants have none
either; if you need a time limit with several waiters, give each waiter its own _s object.
Sleeping
SleepTask, or Task::SleepFor inside a task class, suspends the
calling task for a duration; its thread runs other tasks meanwhile. Nothing cuts a sleep short: to
wait for something with a time limit, wait on it with a timeout instead of sleeping in a loop.
SleepFor is not named Sleep so that it does not hide ::Sleep in classes derived from
Task.
Starting a task later
A Task carries its own timer storage, so any task object can be started by time, for example this one:
// A task that saves the document; it can be started by time.
class AutoSave : public TC::Tasks::WaitableTask_s
{
public:
void Run() noexcept override { SaveDocument(); }
};From inside a task, Task::Schedule with a delay adds the task to the calling task's scheduler once the delay has passed:
// Called from a task: start the save in 50 ms on the calling task's scheduler.
static bool SaveLater(AutoSave& save) noexcept
{
// false: timers not enabled, or the task cannot be started by time
return save.Schedule(TC::TaskPriority::Normal, TC::Duration::Milliseconds(50));
}From a thread that does not run on the scheduler, call
ITaskScheduler::AddTimedTask on the scheduler object. Both
return false if timers are not enabled, if the task has no timer storage
(GetTimerData returns nullptr), if it is being cancelled, or if the
scheduler cannot start tasks by time (the TaskSwap scheduler). Called again before the task has
started, they move the start.
Important
The task object must stay alive from AddTimedTask or Schedule until its OnTaskExited has been
called, as for any task, and it may be added again only after that. A timed task that is waiting
to start counts as added.
Cancelling or advancing a start
CancelTimedTask (or Task::CancelSchedule) takes
back a start that has not happened yet. It returns true if the task was waiting to start and
false if it had started already or was not timed. A cancelled task exits without running: its
OnTaskExited is called on its scheduler, so a waiter of a WaitableTask_s or a task group is
released as usual:
// From outside the scheduler: plan the save for ten seconds from now, then
// take it back.
AutoSave save;
bool cancelled = false;
if (scheduler->AddTimedTask(&save, TC::TaskPriority::Normal,
TC::Duration::Seconds(10))) {
cancelled = TC::CancelTimedTask(&save); // same as save.CancelSchedule()
save.Wait(); // passes either way: a cancelled task exits without
// running
}ScheduleTimedTask (or Task::ScheduleNow) starts a
waiting task now instead of later; it returns false if the task was not waiting to start:
// A save planned for later that is needed now.
AutoSave save;
if (scheduler->AddTimedTask(&save, TC::TaskPriority::Normal,
TC::Duration::Seconds(10))) {
TC::ScheduleTimedTask(&save); // same as save.ScheduleNow()
save.Wait();
}Neither call blocks.
Disabling timers
TC::DisableTimers(); // the last call stops the timer threadCall it once all timed work is done. The last DisableTimers ends everything
that is still waiting: tasks not yet started exit without running (their OnTaskExited is called
on their scheduler, which must still be running), sleeping tasks wake, and waits with a timeout time out. No other thread may use timers
meanwhile. It returns false if timers were not enabled.
The program prints:
loaded in time: yes
loading the settings takes too long
timed out: yes
saved later: yes
cancelled: yes, not saved
started now: yes
sleep after disable: returned false
OKA timeout or a delayed start never ends noticeably early, but may end a little late. The program checks the times with 1 ms tolerance, because it measures in whole milliseconds and with another clock than the timer thread.