• Pl chevron_right

      Christian Hergert: Limiters in libdex

      news.movim.eu / PlanetGnome • 11:41 • 2 minutes

    Libdex now has DexLimiter , a small utility for bounding how much asynchronous work runs at once.

    This is useful when a workload can produce more parallelism than the underlying machine, subsystem, or service should actually handle. Common examples include indexing files, downloading URLs, generating thumbnails, parsing documents, or querying a service with a fixed concurrency budget.

    The usual API is dex_limiter_run() . It acquires a permit, starts a fiber, and releases the permit when that fiber finishes.

    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);
    

    In this example, no more than eight file loads will run at the same time, regardless of how many files are queued. The returned DexFuture resolves or rejects with the result of the spawned fiber.

    One important detail is that dropping the returned future does not cancel a fiber that has already started. Once work has acquired a permit, it is allowed to complete so that the limiter can release the permit cleanly.

    For more specialized cases, DexLimiter also supports manual acquire and release:

    g_autoptr(GError) error = NULL;
    
    if (dex_await (dex_limiter_acquire (limiter), &error))
      {
        do_limited_work ();
        dex_limiter_release (limiter);
      }
    

    This is useful when the limited section is not naturally represented by a single fiber. However, callers must release exactly once for every successful acquire. In most cases, dex_limiter_run() is preferable because it handles release on both success and failure paths.

    The limit should describe the constrained resource, not the number of items being processed. Remote APIs and databases may need a small limit. CPU-heavy work should usually be near the amount of useful worker parallelism. Local I/O can often tolerate a larger value, depending on the storage system. Separate resources should usually have separate limiters, so one workload does not consume another workload’s concurrency budget.

    Finally, dex_limiter_close() can be used during shutdown. Once closed, 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.

    The goal is to make bounded parallelism simple: queue as much asynchronous work as you need, but only run as much of it as the system should handle.