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

Events

An event is a flag that tasks wait for. AutoResetEvent lets one waiter pass per Set and resets itself; ManualResetEvent stays set until Reset and lets every waiter pass. Each comes in two variants: the plain name for any number of waiting tasks, and the _s name for at most one waiting task at a time, which is cheaper, accepts a timeout and may be destroyed right after the wait. The examples come from guide-events.cpp, a complete program that checks its own results.

What the members do

Member AutoResetEvent(_s) ManualResetEvent(_s)
Set Resumes one suspended waiter, which takes the signal (the event stays reset); with no waiter the event stays set Sets the event and resumes every suspended waiter
Wait Takes the signal, suspending until a Set if there is none Returns at once while the event is set, else suspends until Set
TryWait Takes the signal if the event is set; true counts as a passed Wait Tells whether the event is set, without changing it
Reset Clears the signal; suspended waiters stay suspended Clears the event; later Wait calls suspend again
Wait(Duration) _s only _s only

The constructor takes isSet: true creates the event set. No member reports errors.

Important

Wait suspends the calling task, so it may only be called from a task or from an OS thread after ConvertThreadToTask. Set, Reset and TryWait never suspend and may be called from any thread, including plain OS threads and the core's I/O thread.

Auto-reset: one pass per signal

An auto-reset event suits a task that sleeps until there is work. This worker handles one request per signal:

// A worker task that sleeps on an auto-reset event and handles one request per
// signal.
class Worker : public TC::Tasks::WaitableTask_s
{
public:
    void Post(Request* request) noexcept
    {
        request_ = request;
        work_.Set(); // wakes the worker, or leaves the event set for its next Wait
    }

    void Quit() noexcept
    {
        request_ = nullptr;
        work_.Set();
    }

    void Run() noexcept override
    {
        for (;;) {
            work_.Wait(); // takes the signal: the event is reset again
            Request* request = request_;
            if (request == nullptr)
                break;
            request->output = request->input * request->input;
            request->done.Set(); // the last access to the request
        }
    }

private:
    AutoResetEvent_s work_; // one waiter: the worker
    Request*         request_ = nullptr;
};

A Set that arrives while no task waits is not lost: the event stays set and the worker's next Wait passes at once. Several Set calls without a waiter in between count as one, so an auto-reset event is a wake-up flag, not a counter:

AutoResetEvent_s event;
event.Set();
event.Set();
event.Set();                         // no waiter: three Sets leave one signal
const bool first  = event.TryWait(); // true: takes the signal
const bool second = event.TryWait(); // false: nothing left

If every signal must be counted, use a Semaphore instead.

With several waiters, AutoResetEvent wakes them in LIFO order (the most recently suspended task first) without fairness. It has no timeouts. A task that never gets a signal because others always pass first is possible under load; design for that or use one waiter per event.

Manual-reset: a gate

A manual-reset event stays set, so it suits one-time conditions such as "initialization done" or "start now". ManualResetEvent lets any number of tasks wait:

// A start gate: every task waits until main opens it.
ManualResetEvent gate;
auto             runner = [&gate]() noexcept {
    gate.Wait(); // any number of waiters
    Work();
};
auto a = TC::Tasks::Task::Create(runner);
auto b = TC::Tasks::Task::Create(runner);
auto c = TC::Tasks::Task::Create(runner);
auto d = TC::Tasks::Task::Create(runner);

TC::Synchronization::TaskGroup_s runners;
TC::Tasks::Task*                 tasks[] = {&a, &b, &c, &d};
for (TC::Tasks::Task* task : tasks) {
    runners.AddTask(task);
    scheduler->AddTask(task);
}
gate.Set(); // resumes all waiting tasks; later Waits pass at once
runners.Wait();

Set resumes every task suspended at that moment; tasks that call Wait later pass at once until someone calls Reset.

Signaling from other threads

Set may be called from a thread that is not a task, for example a thread of a third-party library or a Windows callback:

// Set may come from any thread: here a plain OS thread that is not a task.
ManualResetEvent_s ready;
int                result = 0;
std::thread        thread([&ready, &result] {
    result = ComputeOnLibraryThread();
    ready.Set();
});
ready.Wait(); // main is a converted thread, so it may wait
thread.join();

The waiting side still has to be a task or a converted thread. The library uses this itself: the core's I/O thread completes asynchronous I/O by calling Set on a ManualResetEvent_s inside the request (see Asynchronous files).

Which variant

  • One waiter at a time: use the _s variant. It keeps the state and the waiter in one word, is cheaper, offers Wait(Duration timeout) and may be destroyed by the waiter as soon as its Wait has returned.
  • Several waiters at the same time: use the plain variant.
  • Each signal must wake exactly one waiter: auto-reset. A condition that stays true: manual-reset.

Important

For the _s variants at most one task may wait at a time. Debug builds assert it; release builds do not check it. See Single-waiter variants.

Destroying an event

The single-waiter variants allow the most common pattern of all: an event on the waiter's stack. The request below carries a ManualResetEvent_s:

// A request lives on the requester's stack; its event tells the requester that it
// is done.
struct Request {
    int                input  = 0;
    int                output = 0;
    ManualResetEvent_s done;
};

The requester waits and returns, which destroys the request together with its event:

// Called by a task (or converted thread). The request and its event are destroyed
// when this returns, right after the Wait: allowed for the single-waiter (_s)
// events.
int Square(Worker& worker, int x) noexcept
{
    Request request;
    request.input = x;
    worker.Post(&request);
    request.done.Wait();
    return request.output;
}

Main, a converted thread, asks the worker for the squares of 1 to 10:

// Start the worker, send it requests, let it quit.
Worker worker;
scheduler->AddTask(&worker);
int sum = 0;
for (int i = 1; i <= 10; ++i)
    sum += Square(worker, i);
worker.Quit();
worker.Wait();

This is safe because the Set that lets the waiter pass is the last access to the event. The rules in full:

Important

  • AutoResetEvent_s / ManualResetEvent_s: the waiter may destroy the event as soon as its Wait has returned (or its TryWait returned true). That covers the waiter and the Set that let it pass; any other Set, Reset or TryWait on the event must be finished by then. After a timed Wait returned false, destroy the event only when no Set can come any more. No task may be waiting on an event that is destroyed.
  • AutoResetEvent / ManualResetEvent: destroy the event only when no task can wait on it or set it any more and the last Set has returned. The return of a Wait alone does not mean that, since Set may still be resuming other waiters.

See Lifetimes and sizing and Performance first.

The program prints:

sum of squares: 385
coalesced:      1 0
through gate:   4
from thread:    7
OK

See also