Limiters
Overview
DexLimiter bounds the number of asynchronous operations that may run
at once. Use it when a workload can produce more parallel work than a service,
subsystem, or machine should handle at the same time.
Typical uses include indexing files, downloading URLs, generating thumbnails, parsing documents, or querying a service with a known concurrency budget.
Running Work
The safest API is dex_limiter_run(). It acquires a permit, spawns a
fiber, and releases the permit when that fiber completes.
static DexFuture *
load_one_file (gpointer user_data)
{
GFile *file = user_data;
return dex_file_load_contents_bytes (file);
}
DexLimiter *limiter = dex_limiter_new (8);
DexFuture *future = dex_limiter_run (limiter,
NULL,
0,
load_one_file,
g_object_ref (file),
g_object_unref);
The returned future resolves or rejects with the result of the spawned fiber.
If many calls to dex_limiter_run() are made, only the configured number
will run at once. Once a fiber has started, dropping the returned future does
not stop the fiber; it is allowed to complete so the permit can be released.
Use dex_limiter_run_on_pool() when the limited work should run on a
DexThreadPool instead of a fiber. This is useful for blocking or
foreign APIs that should not run on a scheduler thread.
static DexFuture *
run_blocking_thumbnailer (gpointer user_data)
{
ThumbnailJob *job = user_data;
return thumbnail_job_run_blocking (job);
}
DexLimiter *limiter = dex_limiter_new (4);
DexThreadPool *pool = dex_thread_pool_new (4);
DexFuture *future = dex_limiter_run_on_pool (limiter,
pool,
run_blocking_thumbnailer,
thumbnail_job_ref (job),
thumbnail_job_unref);
The returned future resolves or rejects with the result of the submitted thread-pool work. Once the work has been submitted, dropping the returned future does not stop the work; it is allowed to complete so the permit can be released.
For stackless async workflows, use dex_limiter_run_coroutine(). This
acquires a permit and then runs a DexCoroutineFunc from a scheduler.
static DexFuture *
load_one_cache (DexCoroutineContext *context,
gpointer user_data)
{
DEX_COROUTINE_BEGIN (context);
DEX_COROUTINE_END ();
}
DexLimiter *limiter = dex_limiter_new (4);
DexFuture *future = dex_limiter_run_coroutine (limiter,
NULL,
load_one_cache,
g_object_ref (key),
g_object_unref);
load_one_cache uses stackless coroutine style and avoids allocating a fiber
stack while still participating in limiter scoping.
Manual Scopes
Use dex_limiter_acquire() and dex_limiter_release() when a permit
must cover a custom scope that is not a single fiber.
g_autoptr(GError) error = NULL;
if (dex_await (dex_limiter_acquire (limiter), &error))
{
do_limited_work ();
dex_limiter_release (limiter);
}
Release exactly once for every successful acquisition. Prefer
dex_limiter_run() when possible because it handles release on both
success and failure paths.
Choosing a Limit
Choose a limit that matches the constrained resource, not the number of items in the queue.
- Use a small value for remote services, databases, and APIs with rate limits.
- Use a value near the number of useful worker threads for CPU-heavy work.
- Use a larger value for I/O-heavy local work if the underlying storage or service benefits from parallelism.
- Keep separate limiters for unrelated resources so one workload does not consume another workload’s budget.
Shutdown
Call dex_limiter_close() when no new work should start. Pending and
future acquisitions reject with DEX_ERROR_SEMAPHORE_CLOSED. Work that
already holds a permit may continue, but releasing after close does not make new
permits available.
If you need to wait for graceful shutdown, call
dex_limiter_close_after_drain(), which both closes the limiter and
returns a future that resolves once all queued acquires have settled and all
running holders have released.