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 leftIf 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
_svariant. It keeps the state and the waiter in one word, is cheaper, offersWait(Duration timeout)and may be destroyed by the waiter as soon as itsWaithas 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 itsWaithas returned (or itsTryWaitreturnedtrue). That covers the waiter and theSetthat let it pass; any otherSet,ResetorTryWaiton the event must be finished by then. After a timedWaitreturnedfalse, destroy the event only when noSetcan 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 lastSethas returned. The return of aWaitalone does not mean that, sinceSetmay 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