Overview

Overview

Libdex is a GNOME library that provides support for futures and fibers in GObject-based applications.

The library attempts to follow well established patterns and terminology around programming with futures.

Libdex depends on GLib 2.68 or newer.

Building

To build a program that uses libdex, you can use the following command to get the CFLAGS and libraries necessary to compile and link.

cc hello.c `pkg-config --cflags --libs libdex-1` -o hello

Use #include <libdex.h> to include support for libdex.

Platform Support

Libdex, including Fiber support, has been known to work on Linux, FreeBSD, Solaris, Illumos, macOS, and Windows.

Support for io_uring is limited to Linux.

Futures and Promises

A DexFuture is the primary object representing work that will complete now or at some point in the future. There are various types of futures, such as a DexPromise or DexTimeout.

See the class hierarchy for other types of futures.

Task Groups

Use DexTaskGroup to manage a structured scope of related futures. Task groups let you close a scope, cancel every tracked child, and await the group as a single future. See Task Groups for details.

Fibers

Libdex has support for fibers which run on a separate stack from your thread stack. They are represented with the DexFiber type which is also a DexFuture.

To create a fiber, use dex_scheduler_spawn() with the appropriate scheduler. For thread pool usage, dex_thread_pool_scheduler_get_default() will get you the default thread pool.

Fibers are scheduled from a GSource in a GMainContext which allows them to run cooperatively with other work.

For lower-overhead async control flow, use a DexCoroutine, which is a stackless alternative to fibers. See Coroutines for the usage pattern.

Awaiting Futures

If you are running on a fiber, you may await completion of a future. This will suspend your fiber stack and yield back to the main thread until that future has completed, rejected, or the fiber has been canceled.

Use dex_await() or it’s variants such as dex_await_object() to await completion of any future. If you are not on a fiber, then the await will fail and the error argument will be set.

Reference Counting

Everything that is a DexObject, include DexFuture may be referenced using dex_ref() and unref’d using dex_unref() or dex_clear().

You may also use g_autoptr(DexFuture) to automate this.

Callbacks

When using fibers is not desired you can manually use callbacks based on the completion or rejection of futures. This is faster than using fibers and does not require the use of a special stack. However, it can be more cumbersome to write when in C.

Use dex_future_then(), dex_future_catch(), and dex_future_finally() to do typical try/catch/finally semantics. Each of these return a new DexFuture that may be further acted upon.

Disowning Futures

When a future is no longer needed and unref’d, it will notify the futures it was depending on that they are no longer requiring their completion. Some future types may use this to cascade work cancellation.

If this is not a behavior you want (e.g. you want the dependent task to always complete), then you may use dex_future_disown() to disown a future ensuring it will always complete or reject without cancellation.

Futures as Sets of Work

A common use of libdex is to await for multiple futures to complete before taking further action. Use dex_future_all(), dex_future_all_race(), dex_future_any(), and dex_future_first() to await multiple futures with different semantics about when the new future will complete.

Limiters

Use DexLimiter when a workload should run with bounded concurrency. dex_limiter_run() is the preferred API for spawning fiber work because it acquires a permit before starting and releases it when the fiber completes.

For custom scopes, use dex_limiter_acquire() and dex_limiter_release(), releasing exactly once for every successful acquisition.

See Limiters for guidance on choosing limits and shutdown.

State Machines

Use DexStateMachine when an object has a finite set of modes and transitions may need asynchronous work. Transition requests are serialized and callbacks run from a fiber, so callbacks can use dex_await() while preserving a declared transition graph.

See State Machines for table setup, landing states, and error handling.

Deadlines and Timeouts

Use dex_future_with_timeout(), dex_future_with_timeout_msec(), dex_future_with_timeout_seconds(), or dex_future_with_deadline() when an operation should either produce its normal result or reject after a time limit.

The returned future resolves or rejects with the wrapped future’s result when the wrapped future completes first. If the timeout wins, the returned future rejects with DEX_ERROR_TIMED_OUT and the wrapped future is discarded. Keep and dex_future_disown() a separate reference to work that must continue after the timeout.

DexFuture *future = dex_future_with_timeout_seconds (load_project (), 10);

Work Queues

In addition to fibers, DexScheduler can also schedule general work items to be run. Use dex_scheduler_push() to push a work item onto that scheduler.

On systems that support double-wide compare-and-swap, this will avoid an allocation for the work item and data pair. They will be placed into the wait-free work queue atomically.

Thread Pools and Schedulers

In many thread pool implementations, the worker thread is only executing work items. In libdex, thread pools can both service short work items and fibers along with longer running timeouts and generalized GSource on the GMainContext. This allows a great deal of flexibility in using futures on the thread pool.

The thread pool is implemented as a global work queue and a series of lock-free work queues per worker thread. Work stealing is also used between thread pools for general purpose work items. However, fibers will never be migrated between threads in order to ensure runtime safety.

Because of the expectation that you will await futures on the thread pool threads, should you need to do long blocking work you may consider using dex_thread_spawn(). This thread may not await like fibers as it is a real operating system thread. You may however block on a future using dex_thread_wait_for().