tree: e76ef2bf2d96096d057d0c006ced852ac495ad4a [path history] [tgz]
  1. include/
  2. test/
  6. ops.c

libasync and friends

This set of libraries defines a C and C++ language interface for initiating asynchronous operations and dispatching their results to callback functions.

The purpose of these libraries is to decouple clients which want to perform asynchronous operations from the message loop implementations which dispatch the results of those operations. This makes it an important building block for other abstractions such as the FIDL bindings.


The async package consists of three libraries:

See also libasync-loop.a which provides a general-purpose implementation of async_dispatcher_t.

Using the asynchronous dispatcher

Waiting for signals

To asynchronously wait for signals, the client prepares an async_wait_t structure then calls async_begin_wait() to register it with the dispatcher. When the wait completes, the dispatcher invokes the handler.

The client can register handlers from any thread but dispatch will occur on a thread of the dispatcher's choosing depending on its implementation.

The client is responsible for ensuring that the wait structure remains in memory until the wait's handler runs or the wait is successfully canceled using async_cancel_wait().

See async/wait.h for details.

#include <lib/async/wait.h>     // for async_begin_wait()
#include <lib/async/default.h>  // for async_get_default_dispatcher()

void handler(async_dispatcher_t* async, async_wait_t* wait,
             zx_status_t status, const zx_packet_signal_t* signal) {
    printf("signal received: status=%d, observed=%d", status, signal ? signal->observed : 0);

zx_status_t await(zx_handle_t object, zx_signals_t trigger) {
    async_dispatcher_t* async = async_get_default_dispatcher();
    async_wait_t* wait = calloc(1, sizeof(async_wait_t));
    wait->handler = handler;
    wait->object = object;
    wait->trigger = trigger;
    return async_begin_wait(async, wait);

Getting the current time

The dispatcher represents time in the form of a zx_time_t. In normal operation, values of this type represent a moment in the ZX_CLOCK_MONOTONIC time base. However for unit testing purposes, dispatchers may use a synthetic time base instead.

To make unit testing easier, prefer using async_now() to get the current time according the dispatcher's time base.

See async/time.h for details.

Posting tasks and getting the current time

To schedule asynchronous tasks, the client prepares an async_task_t structure then calls async_post_task() to register it with the dispatcher. When the task comes due, the dispatcher invokes the handler.

The client can post tasks from any thread but dispatch will occur on a thread of the dispatcher's choosing depending on its implementation.

The client is responsible for ensuring that the task structure remains in memory until the task's handler runs or the task is successfully canceled using async_cancel_task().

See async/task.h for details.

#include <lib/async/task.h>     // for async_post_task()
#include <lib/async/time.h>     // for async_now()
#include <lib/async/default.h>  // for async_get_default_dispatcher()

typedef struct {
    async_task_t task;
    void* data;
} task_data_t;

void handler(async_dispatcher_t* async, async_task_t* task, zx_status_t status) {
    task_data_t* task_data = (task_data_t*)task;
    printf("task deadline elapsed: status=%d, data=%p", status, task_data->data);

zx_status_t schedule_work(void* data) {
    async_dispatcher_t* async = async_get_default_dispatcher();
    task_data_t* task_data = calloc(1, sizeof(task_data_t));
    task_data->task.handler = handler;
    task_data->task.deadline = async_now(async) + ZX_SEC(2);
    task_data->data = data;
    return async_post_task(async, &task_data->task);

Delivering packets to a receiver

Occasionally it may be useful to register a receiver which will be the recipient of multiple data packets instead of allocating a separate task structure for each one. The Zircon port takes care of storing the queued packet data contents until it is delivered.

The client can queue packets from any thread but dispatch will occur on a thread of the dispatcher's choosing depending on its implementation.

The client is responsible for ensuring that the receiver structure remains in memory until all queued packets have been delivered.

See async/receiver.h for details.

#include <lib/async/receiver.h>  // for async_queue_packet()
#include <lib/async/default.h>   // for async_get_default_dispatcher()

void handler(async_dispatcher_t* async, async_receiver_t* receiver, zx_status_t status,
             const zx_packet_user_t* data) {
    printf("packet received: status=%d, data.u32[0]=%d", status, data ? data.u32[0] : 0);

const async_receiver_t receiver = {
    .state = ASYNC_STATE_INIT,
    .handler = handler;

zx_status_t send(const zx_packet_user_t* data) {
    async_dispatcher_t* async = async_get_default_dispatcher();
    return async_queue_packet(async, &receiver, data);

Verifying synchronization requirements

Thread-unsafe types require accesses to its methods and fields to be externally synchronized. To enforce this guarantee, one may be tempted to record the thread ID each time the thread-unsafe object is used, and check that they stay the same. This is an anti-pattern because async dispatchers may support sequences: sequential execution domains which runs a series of tasks with strict mutual exclusion, but where the underlying execution may hop from one thread to another. Checking thread IDs would fail whenever a sequence is migrated to a different underlying thread, even if those threads never execute in parallel.

When a dispatcher supports sequences, one may call async_get_sequence_id to obtain an identifier for the currently running sequence, and verify external synchronization by checking that they stay the same. See async/sequence_id.h for details.

When a dispatcher does not support sequences, falling back to checking thread identifiers is usually the next best alternative. async/cpp/sequence_checker.h provides a BasicLockable type that implements this fallback and could be used in thread-unsafe code to improve safety.

The default async dispatcher

As a client of the async dispatcher, where should you get your async_dispatcher_t* from?

The ideal answer is for the async_dispatcher_t* to be passed into your code when it is initialized. However sometimes this becomes burdensome or isn't practical.

For this reason, the shared library provides functions for getting or setting a thread-local default async_dispatcher_t* using async_get_default_dispatcher() or async_set_default_dispatcher().

This makes it easy to retrieve the async_dispatcher_t* from the ambient environment by calling async_get_default_dispatcher(), which is used by many libraries.

Message loop implementations should register themselves as the default dispatcher any threads they service.

See async/default.h for details.

Using the C++ helpers

libasync-cpp.a includes helper classes such as Wait, Task, and Receiver which wrap the C API with a more convenient type safe interface for use in C++.

Note that the C API can of course be used directly from C++ for special situations which may not be well addressed by the wrappers.

Implementing a dispatcher

The async_ops_t interface is a low-level abstraction for asynchronous dispatchers. You can make custom implementations of this interface to integrate clients of this library with your own dispatcher.

It is possible to implement only some of the operations but this may cause incompatibilities with certain clients.

See async/dispatcher.h for details.