Our Asynchrony Model#
The previous pages (Why Asynchrony? and Design Considerations in amongoc) explored why we want to provide asynchrony, and this page will explain how we provide asynchrony.
Emitters & Handlers#
The asynchrony model in amongoc
is defined by emitters and
handlers.
Emitters#
An emitter object, defined by amongoc_emitter
, represents a pending
asynchronous operation, where “operation” may be anything from “send some data
to a server” to your entire application.
An emitter is said to resolve or complete when its associated asynchronous
operation finishes. In amongoc
, a resolving emitter emits two objects: an
amongoc_status
representing the success/error/etc of the operation, and an
amongoc_box
that encloses the final “result value” for the operation. The
meaning of the result value depends on the asynchronous operation of the emitter
and the value of the result status.
For example, the amongoc_client_new()
function returns an amongoc_emitter
that resolves with an amongoc_client
upon success (this is represented in
the documentation with the [[type(…)]]
attribute).
Handlers#
Tip
Writing a valid amongoc
handler is somewhat subtle and requires
care. Usually, it is easier to use the library’s asynchronous operation
composition APIs instead of writing a handler directly.
A handler combines with a emitter to form to tell that emitter how to continue after its associated asynchronous operation completes.
Accessing an Emitter’s Result#
There is no way to “ask” an emitter for its result value. Instead, an emitter must be connected to a handler that acts on the completion of the emitter.
However, using amongoc_handler
directly is a very low-level and error-prone
process. For this reason, amongoc
provides convenience functions for the
purpose of composing emitters automatically without needing to create and juggle
amongoc_handler
s oneself, such as amongoc_then()
, amongoc_let()
,
amongoc_detach()
, and amongoc_tie()
.
Prior Art - Senders & Receivers#
The asynchronous model provided by amongoc
is based on a highly-simplified
version of P2300 - std::execution, the leading proposal for defining a
universal asynchronous execution design for C++.
The differences are many, mostly by omission, but the most important changes are as follows (if you are unfamiliar with P2300, you can ignore these details):
In
amongoc
, senders and receivers are called emitters and handlers, respectively. This name change is designed to prevent confusion between the two designs.In
amongoc
, the scheduler mechanism of senders is absent.In
amongoc
, because we are a C library, all emitters and handlers are type-erased to single struct types,amongoc_emitter
andamongoc_handler
.Emitters always emit two values: an
amongoc_status
and anamongoc_box
, which also type-erases the result type. The actual emitted result type is a matter of documentation for the associated operation.In
amongoc
, emitters have one completion channel, whereas senders have three (“value”, “error”, and “stopped”). Emitters transmit the success/error/cancellation state via theiramongoc_status
value.amongoc
has the concept of operation cancellation, but does not use stop tokens. Instead, anamongoc_handler
usesamongoc_handler_vtable::register_stop()
to connect stop callbacks for an operation.