Dispatchers enable drivers to schedule asynchronous work on threads available in a driver host. These shared threads, managed by the driver runtime, allow for better overall performance and thread safety among drivers in a driver host. Drivers are strongly discouraged from spawning their own threads.
Fuchsia’s driver framework offers
fdf::Dispatcher
, which is the recommended dispatcher to be
used with Fuchsia drivers. This driver dispatcher supports the following
features:
- Enforce a specified threading model.
- Enforce no reentrancy.
- Enable driver transport FIDL for in-process driver-to-driver communication.
- Provide an implementation of the
async
library as an interface for initiating asynchronous operations.
Dispatcher operations
A dispatcher, assigned to a driver, coordinates and performs asynchronous operations for the driver. All dispatchers in a driver host (therefore, in the same process) are backed by a pool of shared threads managed by the driver runtime. Dispatchers can only operate within a driver runtime environment, such as a driver host or a driver testing framework.
A dispatcher mainly handles two tasks: it schedules asynchronous work to run on threads in a driver host (in response to a callback created by a driver or a driver host) and it actually runs the work on those threads on behalf of a driver (or “calls into a driver”).
A dispatcher may call into a driver in the following common scenarios:
- A driver hook is run (see Default dispatcher).
- A callback for a previously registered asynchronous operation is run.
- The dispatcher is shutting down (see Shutting down dispatchers).
Threading model
Dispatchers in a driver host may be created with different options to configure their own threading model for:
- Making parallel calls into a driver (see Synchronized and unsynchronized)
- Scheduling blocking operations (see Synchronous operations)
Synchronized and unsynchronized
Created with the FDF_DISPATCHER_OPTION_SYNCHRONIZED
option, synchronized
dispatchers never make parallel calls into a driver. This may be thought of as a
single-threaded dispatcher, with the caveat that it makes no guarantee that the
dispatcher calls into a driver from the same thread each time
(see Thread local storage).
Created with the FDF_DISPATCHER_OPTION_UNSYNCHRONIZED
option, unsynchronized
dispatchers may make parallel calls into a driver. However, with this option,
there are no ordering guarantees; the driver is in charge of all synchronization
of state. Therefore, when possible, it is preferable to use a synchronized
dispatcher to avoid such complexity.
Figure 1. A timeline of callbacks scheduled by synchronized and unsynchronized dispatchers.
As shown in Figure 1, the synchronized dispatcher shows no parallel callbacks while the unsynchronized dispatcher shows multiple parallel callbacks.
Synchronous operations
In general, drivers are discouraged from making synchronous calls because
they can block other tasks from running. However, if necessary, a driver
can create a dispatcher with the FDF_DISPATCHER_OPTION_ALLOW_SYNC_CALLS
option, which is only supported for
synchronized dispatchers.
This option spawns an additional thread in the thread pool when the dispatcher is created. This helps the driver runtime mitigate the chances that synchronous calls introduced by the driver would block other drivers in the same driver host.
Figure 2. A timeline of callbacks scheduled by synchronized dispatchers with
and without the FDF_DISPATCHER_OPTION_ALLOW_SYNC_CALLS
option.
In Figure 2, the boxes on the left (Task A1 and Task A2) represent non-blocking tasks while the boxes on the right (Task B1 and Task B2) represent blocking tasks. Thread 1 and 2 are shared by the dispatchers in a driver host to run both blocking and non-blocking tasks. Task B1 is running a synchronous call, blocking other tasks from scheduled on Thread 1. However, the driver runtime is free to schedule asynchronous tasks on Thread 2 while Thread 1 is in use.
Thread local storage
A dispatcher may call into a driver from any shared thread managed by the driver
runtime. However, the driver runtime makes no guarantee that it will be the same
thread per call. For this reason, drivers should not use any thread-local
storage (for instance, using the thread_local
keyword in C++
) since such
storages tie the lifetime of variables to a distinct thread.
Reentrancy guarantees
A dispatcher never makes reentrant calls into a driver – a call is considered reentrant if the driver has previously received a call from the dispatcher in the same call stack. If a call would be reentrant, the dispatcher schedules it to occur on a future iteration of the dispatcher loop.
Lifetime of a dispatcher
A driver host manages the lifetime of a driver's default dispatcher. The driver host guarantees that this dispatcher is not destroyed until the driver is stopped.
Default dispatcher
In DFv2, a dispatcher is provided to a driver as part of the driver’s start hook
(that is, the Start()
function in the driver’s code). This becomes the default
dispatcher for the driver. To retrieve this dispatcher, the driver may call the
fdf::Dispatcher::GetCurrent()
function during a driver hook, such as
Start()
, PrepareStop()
, and Stop()
.
In DFv1, a driver host creates a new dispatcher for a driver when it is bound.
To retrieve this dispatcher, the driver may call the
fdf::Dispatcher::GetCurrent()
function during a driver hook or
device hook, such as Bind()
, Unbind()
, and Release()
.
Shutting down dispatchers
In DFv2, the driver host automatically shuts down all dispatchers for a driver
before the driver’s Stop()
hook is called. If a driver wishes to be notified
before the shutdown occurs, the driver may implement the PrepareStop()
hook.
In DFv1, a driver’s default dispatcher automatically shuts down after the
Unbind()
hook in the driver’s main device is called (which also causes all
child devices to be unbound), but before the Release()
hook is called – the
driver's main device is the device added by the driver's Bind()
hook. The
driver must handle the shutting down of any additional dispatchers it has created.
When a dispatcher is shutting down, it dispatches all pending callbacks with the
ZX_ERR_CANCELED
status and calls the shutdown handler that was provided to
fdf::Dispatcher::Create()
.
Creating additional dispatchers
A driver can only create an additional dispatcher during a callback from a
dispatcher, that is, when a dispatcher calls into a driver (see
Dispatcher operations for examples of when this occurs).
Unlike the default dispatcher, the driver owns and manages the lifetime of these
additional dispatchers. In DFv2, the driver host automatically shuts down a
driver’s additional dispatchers before the driver’s Stop()
hook is called.