The dispatcher scheduler
On this page
A UI thread runs a message loop and must never block. A Default scheduler cannot help directly, because its tasks run on its own threads, and a converted thread blocks when it waits. The dispatcher scheduler runs tasks on a thread that has an event loop of its own, typically a UI thread. Its tasks can wait like any other task, and the thread keeps running its loop meanwhile.
Event handlers become ordinary functions that wait:
void OnRender() noexcept // runs as a task of the dispatcher scheduler
{
TC::Tasks::RunAndWait(workers, [&] { Render(); }); // the UI keeps running meanwhile
ShowImage(); // UI thread again
}This needs no co_await, no IAsyncAction and no callbacks. Functions called from the handler
may wait as well, at any depth.
How it works
CreateDispatcherScheduler creates the scheduler and binds it to the calling thread. The scheduler has no threads of its own: the event loop drives it.
| Step | What happens |
|---|---|
| An event arrives | The window procedure calls RunTask. The handler runs at once, as a task on its own fiber. |
| The handler waits | It gives control back. RunTask returns and the window procedure returns: the message loop goes on. |
| The awaited work finishes | Whoever completes it (a worker task, a timer, async I/O) resumes the handler. The scheduler passes the handler to the DispatcherWakeProc, which posts it to a window as a message. |
| The message arrives | The window procedure calls ContinueTask. The handler goes on, on the UI thread, until it ends or waits again. |
The message queue is the ready queue. Every continuation is one message, so input and painting get their turn between two continuations. Handlers continue in the order in which they were resumed: a handler that started later may finish first.
A task returns to whoever entered it
A task of the dispatcher scheduler always gives control back to the code that started or continued it last. That code is a window procedure, another dispatcher task, or a modal loop running inside one:
- Every window procedure gets control back before it returns itself. Window procedures that Windows calls from inside its own calls return in the right order.
- A handler may raise further events synchronously, for example
SetWindowPossendsWM_SIZE. A handler started from there returns to the first handler. - Modal loops, such as a message box or moving the window, dispatch the messages as well. Waiting handlers go on there too. This is why the wake callback posts to a window: a thread message would be dropped by a modal loop.
The thread stays a thread
The bound thread is not converted into a task. Outside the scheduler's tasks it is an ordinary thread: the message loop, synchronous message handlers and the rest of the program run as before. Only the tasks can wait. The thread itself never blocks.
- GetTaskScheduler returns the dispatcher inside its tasks only.
- Task::Schedule without a scheduler adds to the calling task's scheduler. In a handler that is the UI thread. Give work for other threads its scheduler explicitly (see Waiting on a UI thread).
- AddTask on the dispatcher works from any thread. It runs a task on the UI thread, for example a worker reporting a result.
- Timers (SleepTask, waits with a timeout, AddTimedTask) work as on any scheduler.
Compared with C++ coroutines
| C++/WinRT coroutines | Dispatcher scheduler | |
|---|---|---|
| A handler that waits | returns fire_and_forget or IAsyncAction, uses co_await |
an ordinary function, waits on a primitive |
| Functions it calls that wait | must be coroutines too | stay ordinary functions |
| Where the state lives | in a heap frame per coroutine | on the task's stack |
| Where the handler goes on | as a new dispatch on the UI thread | as a new dispatch on the UI thread |
| What the framework must do | nothing: the compiler transforms the handler | start the handler with RunTask instead of calling it |
Both models return to the message loop at the wait, and both continue as a new dispatch. A second
click during the wait starts the handler again in both, as with await in C#.
The dispatcher scheduler needs a framework that raises the events itself, so that it can start each
asynchronous handler with RunTask. For WinUI/XAML, use C++/WinRT coroutines on the UI thread, and
WinRT::Await in tasks of other schedulers.
Rules
Important
- Wait only inside tasks of the dispatcher scheduler, never in other code on the bound thread.
- Every task the wake callback receives must reach ContinueTask exactly once. Destroy the window that receives them only once no task waits any more.
- Do not wait inside an activation context of your own (
ActivateActCtx… wait …DeactivateActCtx), such as MFC'sAFX_MANAGE_STATE. A task runs in the activation context of whoever entered it, like a function called from there. - Stop and destroy the scheduler on the bound thread, once no task waits.