Cooperative scheduling. More...
Modules | |
Signal | |
Waiting for an event to complete. | |
Monitor | |
Monitor a coroutine for termination. | |
Data Structures | |
struct | bio_coro_t |
Handle to a coroutine. More... | |
struct | bio_monitor_t |
Handle to a monitor. More... | |
struct | bio_cls_t |
Coroutine-local storage (CLS) specification. More... | |
struct | bio_coro_options_t |
Coroutine spawn options. More... | |
Typedefs | |
typedef void(* | bio_entrypoint_t) (void *userdata) |
Entrypoint for a coroutine. | |
Enumerations | |
enum | bio_coro_state_t { BIO_CORO_READY , BIO_CORO_RUNNING , BIO_CORO_WAITING , BIO_CORO_DEAD } |
State of a coroutine. More... | |
Functions | |
bio_coro_t | bio_spawn_ex (bio_entrypoint_t entrypoint, void *userdata, const bio_coro_options_t *options) |
Spawn a new coroutine. | |
static bio_coro_t | bio_spawn (bio_entrypoint_t entrypoint, void *userdata) |
Spawn a new coroutine. | |
bio_coro_state_t | bio_coro_state (bio_coro_t coro) |
Check the state of a coroutine. | |
bio_coro_t | bio_current_coro (void) |
Get the handle of the currently running coroutine. | |
void | bio_yield (void) |
Let a different coroutine run. | |
void | bio_set_coro_data (void *data, const bio_tag_t *tag) |
Associate the calling coroutine with arbitrary data and a tag. | |
void * | bio_get_coro_data (bio_coro_t coro, const bio_tag_t *tag) |
Retrieve the data associated with a coroutine. | |
void | bio_set_coro_name (const char *name) |
Give a human-readable name to the calling coroutine. | |
const char * | bio_get_coro_name (bio_coro_t coro) |
Get a human-readable name for the coroutine if it has one. | |
void * | bio_get_cls (const bio_cls_t *cls) |
Get a coroutine-local storage (CLS) object. | |
Cooperative scheduling.
At the heart of bio is a cooperative scheduling system. Coroutines are generally cheaper to spawn and context switch compared to threads.
Instead of using callbacks, all I/O calls in bio will block the calling coroutine. A different coroutine will be scheduled to run and the original coroutine will only resume once the original call return.
Coroutines can interact with one another through:
typedef void(* bio_entrypoint_t) (void *userdata) |
Entrypoint for a coroutine.
enum bio_coro_state_t |
State of a coroutine.
Enumerator | |
---|---|
BIO_CORO_READY | The coroutine is ready to run but not running yet. |
BIO_CORO_RUNNING | The coroutine is the currently running coroutine that called bio_coro_state. |
BIO_CORO_WAITING | The coroutine is waiting for a signal. |
BIO_CORO_DEAD | The coroutine has terminated. |
bio_coro_state_t bio_coro_state | ( | bio_coro_t | coro | ) |
Check the state of a coroutine.
bio_coro_t bio_current_coro | ( | void | ) |
Get the handle of the currently running coroutine.
void * bio_get_cls | ( | const bio_cls_t * | cls | ) |
Get a coroutine-local storage (CLS) object.
This is used when something needs to be (lazily) created once in each coroutine. The CLS spec should be a global variable in a .c/.cpp file and passed as a pointer:
The memory for the CLS object will only be allocated the first time a coroutine calls this function. The optional bio_cls_t::init callback will be invoked to initialize this memory block.
Subsequent calls to bio_get_cls
from the same coroutine with the same cls
pointer will return the same object.
When a coroutine terminates, the optional bio_cls_t::cleanup callback will be invoked. The associated memory will be automatically freed.
Internally, the default logger use CLS to allocate a private format buffer for each coroutine. This avoids the problem of corrupting log content when a coroutine inevitably gets context-switched since logging would involve some sort of I/O.
cls | The specification for the CLS. Using the same pointer will return the same CLS object. |
void * bio_get_coro_data | ( | bio_coro_t | coro, |
const bio_tag_t * | tag | ||
) |
Retrieve the data associated with a coroutine.
This returns the data that was set with bio_set_coro_data.
If bio_set_coro_data was never called by the target coroutine, this will return NULL
.
If the tag
is not the same as what was previously passed to bio_set_coro_data, this will return NULL
.
If the targeted coro
has terminated, this will also return NULL
.
Typically, the associated data is stack-allocated by the owning coroutine. Since it is safe to call this on a dead coroutine, it is unnecessary and even slightly less efficient to check a coroutine's liveness before calling this function.
coro | The coroutine to retrieve data from |
tag | A unique tag |
const char * bio_get_coro_name | ( | bio_coro_t | coro | ) |
Get a human-readable name for the coroutine if it has one.
This may return NULL if a name was not given.
void bio_set_coro_data | ( | void * | data, |
const bio_tag_t * | tag | ||
) |
Associate the calling coroutine with arbitrary data and a tag.
This data can later be retrieved by any coroutine, provided that they pass the same tag to bio_get_coro_data. This allows a coroutine to quickly expose its states to others.
"Module-private" data can be implemented by declaring the tag as a static global variable in a .c/.cpp file. Hence, only functions in the same file can call bio_set_coro_data and bio_get_coro_data.
This is provided as an additional runtime safety check. Since all bio_coro_t handles are interchangable and the user data is just an untyped void*
pointer, it is possible to incorrectly pass the wrong coroutine to the wrong function that does not know how to cast the pointer back to the correct concrete type, resulting in undefined behaviour. The tag acts as a type check and access check on the data.
data | An arbitrary pointer |
tag | A unique tag |
void bio_set_coro_name | ( | const char * | name | ) |
Give a human-readable name to the calling coroutine.
Using the default logger, this name will be displayed instead of the coroutine handle's numeric representation (e.g: main
instead of 1:0
).
It is by design that a coroutine can only name itself, hence the lack of a coro
argument in this function.
|
inlinestatic |
Spawn a new coroutine.
This is equivalent to:
entrypoint | The entrypoint for the coroutine. |
userdata | Data to pass to the entrypoint. |
bio_coro_t bio_spawn_ex | ( | bio_entrypoint_t | entrypoint, |
void * | userdata, | ||
const bio_coro_options_t * | options | ||
) |
Spawn a new coroutine.
entrypoint | The entrypoint for the coroutine. |
userdata | Data to pass to the entrypoint. |
options | Option for the new coroutine, can be NULL . |
void bio_yield | ( | void | ) |
Let a different coroutine run.
Since cooperative scheduling is used in the main thread, if a coroutine is doing something intensive, it could block other coroutines from running. Calling this function will let other coroutines run. However, it is probably a better idea to use the async thread pool instead.
Another use for this would be to implement a busy wait.