Preliminary. ThinkMeta ConcurrentTasks and this documentation are preliminary; everything is subject to change.

Timeouts

Everything that has to do with time runs on the core's timer thread: waits with a timeout, sleeping tasks and tasks started after a delay. This sample turns the timer thread on, lets a task wait on an AutoResetEvent_s with and without a signal, sleeps, starts a task by time, takes back a timed start and turns the timers off again. It measures how long each step took.

Turning timers on

// Without the timer thread a wait with a timeout waits forever and SleepFor
// returns false at once.
if (!TC::EnableTimers())
    return 1;

EnableTimers starts the timer thread; calls are counted, so a library and the application can both enable it.

The objects

// Only the demo task waits on the event, so the single-waiter variant (with
// timeouts) fits.
TC::Synchronization::AutoResetEvent_s event;
bool                                  setterRan    = false;
bool                                  cancelledRan = false;
auto                                  setter       = Task::Create([&] {
    setterRan = true;
    event.Set();
});
auto cancelled = Task::Create([&] { cancelledRan = true; });

A wait that times out

// 1. Nobody sets the event: the wait ends after the timeout and returns
// false.
auto start = Clock::now();
timedOut   = !event.Wait(Duration::Milliseconds(50));
std::printf("Wait without Set: %s after %lld ms\n",
            timedOut ? "timed out" : "signaled",
            MillisecondsSince(start));

Wait with a Duration returns false when the time is up. Only the single-waiter variants offer a timeout.

A task started by time

// 2. A task started by time sets the event after 100 ms; the wait with
// a 1 s limit
//    returns true as soon as it does. Schedule with a delay must be
//    called from a task and fails only without EnableTimers.
start = Clock::now();
if (setter.Schedule(TaskPriority::Default, Duration::Milliseconds(100)))
    signaled = event.Wait(Duration::Seconds(1));
std::printf("Wait with a timed Set: %s after %lld ms\n",
            signaled ? "signaled" : "timed out",
            MillisecondsSince(start));

Schedule with a delay adds the task to the calling task's scheduler once the delay has passed. From outside a task, AddTimedTask on the scheduler does the same.

Sleeping

// 3. Sleeping suspends only this task; its thread runs other tasks
// meanwhile.
start = Clock::now();
slept = Task::SleepFor(Duration::Milliseconds(20));
std::printf("SleepFor 20 ms: returned after %lld ms\n",
            MillisecondsSince(start));

SleepFor suspends only the calling task. Nothing cuts a sleep short; to wait for something with a limit, wait on it with a timeout.

Cancelling a timed start

// 4. A start in 10 s that is taken back: the task exits without
// running.
cancelAccepted
    = cancelled.Schedule(TaskPriority::Default, Duration::Seconds(10))
    && cancelled.CancelSchedule();
std::printf("Timed start cancelled: %s\n",
            cancelAccepted ? "yes" : "no");

CancelSchedule takes back a start that has not happened yet. The task does not run, but it still exits: its exit notification is called, so a group it belongs to counts it out.

Joining and shutting down

// A cancelled task still reports its exit, so the group counts it out like
// the others.
TC::Synchronization::TaskGroup_s group;
group.AddTask(&demo);
group.AddTask(&setter);
group.AddTask(&cancelled);
scheduler->AddTask(&demo);
group.Wait();
// Timers off while the scheduler still runs: a timed task that has not
// started yet would exit on its scheduler now.
TC::DisableTimers();
scheduler->Stop();

Full program

// Timeouts: a wait with a time limit, a sleeping task, a task started later by time
// and a timed start that is cancelled. All of it needs the core's timer thread
// (EnableTimers).
#define NOMINMAX
#include <Windows.h>

#include <chrono>
#include <cstdio>

#include <ThinkMeta.ConcurrentTasks/Core.h>
import ThinkMeta.ConcurrentTasks.Synchronization.AutoResetEvent;
import ThinkMeta.ConcurrentTasks.Synchronization.TaskGroup;
import ThinkMeta.ConcurrentTasks.Tasks.Task;

namespace TC = ThinkMeta::ConcurrentTasks;
using TC::Duration;
using TC::TaskPriority;
using TC::Tasks::Task;

namespace {
    using Clock = std::chrono::steady_clock;

    long long MillisecondsSince(Clock::time_point start) noexcept
    {
        return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now()
                                                                     - start)
            .count();
    }
} // namespace

int main()
{
    TC::ConvertThreadToTask();
    // Without the timer thread a wait with a timeout waits forever and SleepFor
    // returns false at once.
    if (!TC::EnableTimers())
        return 1;

    TC::SchedulerInfo   info{};
    TC::ITaskScheduler* scheduler
        = TC::CreateTaskScheduler(TC::SchedulerKind::Default, &info);
    if (scheduler == nullptr)
        return 1;
    scheduler->Start();

    bool ok = false;
    {
        // Only the demo task waits on the event, so the single-waiter variant (with
        // timeouts) fits.
        TC::Synchronization::AutoResetEvent_s event;
        bool                                  setterRan    = false;
        bool                                  cancelledRan = false;
        auto                                  setter       = Task::Create([&] {
            setterRan = true;
            event.Set();
        });
        auto cancelled = Task::Create([&] { cancelledRan = true; });

        bool timedOut       = false;
        bool signaled       = false;
        bool slept          = false;
        bool cancelAccepted = false;

        auto demo = Task::Create([&] {
            // 1. Nobody sets the event: the wait ends after the timeout and returns
            // false.
            auto start = Clock::now();
            timedOut   = !event.Wait(Duration::Milliseconds(50));
            std::printf("Wait without Set: %s after %lld ms\n",
                        timedOut ? "timed out" : "signaled",
                        MillisecondsSince(start));

            // 2. A task started by time sets the event after 100 ms; the wait with
            // a 1 s limit
            //    returns true as soon as it does. Schedule with a delay must be
            //    called from a task and fails only without EnableTimers.
            start = Clock::now();
            if (setter.Schedule(TaskPriority::Default, Duration::Milliseconds(100)))
                signaled = event.Wait(Duration::Seconds(1));
            std::printf("Wait with a timed Set: %s after %lld ms\n",
                        signaled ? "signaled" : "timed out",
                        MillisecondsSince(start));

            // 3. Sleeping suspends only this task; its thread runs other tasks
            // meanwhile.
            start = Clock::now();
            slept = Task::SleepFor(Duration::Milliseconds(20));
            std::printf("SleepFor 20 ms: returned after %lld ms\n",
                        MillisecondsSince(start));

            // 4. A start in 10 s that is taken back: the task exits without
            // running.
            cancelAccepted
                = cancelled.Schedule(TaskPriority::Default, Duration::Seconds(10))
                && cancelled.CancelSchedule();
            std::printf("Timed start cancelled: %s\n",
                        cancelAccepted ? "yes" : "no");
        });

        // A cancelled task still reports its exit, so the group counts it out like
        // the others.
        TC::Synchronization::TaskGroup_s group;
        group.AddTask(&demo);
        group.AddTask(&setter);
        group.AddTask(&cancelled);
        scheduler->AddTask(&demo);
        group.Wait();

        ok = timedOut && signaled && setterRan && slept && cancelAccepted
            && !cancelledRan;
        std::printf("%s\n", ok ? "ok" : "UNEXPECTED RESULT");

        // Timers off while the scheduler still runs: a timed task that has not
        // started yet would exit on its scheduler now.
        TC::DisableTimers();
        scheduler->Stop();
    }

    scheduler->Destroy();
    TC::ConvertTaskToThread();
    return ok ? 0 : 1;
}

Source and project on GitHub

Build and run

A console program. With the core's props file imported and the library referenced (see Set up a project) it needs nothing else. It takes about a fifth of a second; the measured times may be a little longer on a busy machine:

Wait without Set: timed out after 50 ms
Wait with a timed Set: signaled after 100 ms
SleepFor 20 ms: returned after 20 ms
Timed start cancelled: yes
ok

What to notice

Important

Without EnableTimers a wait with a timeout waits forever, SleepFor returns false at once and a start by time fails. Turn the timers on before the first task relies on them.

  • A duration is a minimum. The timer fires on a grid of about half a millisecond on current Windows versions; a task may start or wake a little later than asked.
  • Single waiter for timeouts. Timeouts exist only on the _s variants (AutoResetEvent_s, Semaphore_s, WaitableTask_s, TaskGroup_s, …). At most one task may wait on such an object at a time.
  • A cancelled task still exits. After CancelSchedule the object must stay alive until its exit notification has run – here the group takes care of that.
  • Timers off before the scheduler stops. DisableTimers lets timed tasks that have not started exit on their scheduler, which must still be running then.
  • Which thread may call what. Schedule with a delay finds the scheduler of the calling task, so it must be called from a task; Set may be called from anywhere.

See also