Skip to content

Synchronization Primitives

uvent::sync provides coroutine-native primitives with zero thread blocking, no heap allocations on the fast path, and fairness suitable for high-concurrency runtimes.

Primitives:

All operations suspend coroutines and re-schedule them through the event-loop queue (system::this_thread::detail::q) instead of blocking OS threads.


AsyncMutex

AsyncMutex is a coroutine-safe synchronization primitive designed for Uvent’s asynchronous runtime.
It allows multiple coroutines to coordinate access to shared resources without blocking threads.

Overview

Unlike traditional std::mutex, AsyncMutex suspends waiting coroutines and re-schedules them via the event loop queue. This avoids kernel-level blocking and keeps CPUs busy with useful work.

Features

  • Fully coroutine-aware (co_await mutex.lock()).
  • Zero blocking — waiters suspend.
  • Single atomic with embedded waiter stack.
  • Fair and efficient LIFO hand-off.
  • Minimal memory footprint — no allocations.
  • Guard-based RAII unlocking.

Example

#include "uvent/sync/AsyncMutex.h"
#include "uvent/Uvent.h"
#include "uvent/system/SystemContext.h"
#include <iostream>

using namespace usub::uvent;
using usub::uvent::sync::AsyncMutex;

static AsyncMutex g_mutex;

task::Awaitable<void> worker(int id)
{
    {
        auto guard = co_await g_mutex.lock();
        std::cout << "Worker " << id << " acquired lock\n";
        co_await system::this_coroutine::sleep_for(std::chrono::milliseconds(300));
        std::cout << "Worker " << id << " released lock\n";
    }
    co_return;
}

API Reference

namespace usub::uvent::sync {

class AsyncMutex {
public:
    struct Guard {
        bool owns_lock() const noexcept;
        void unlock() noexcept;
    };

    struct LockAwaiter {
        bool await_ready() noexcept;
        bool await_suspend(std::coroutine_handle<> h) noexcept;
        Guard await_resume() noexcept;
    };

    LockAwaiter lock() noexcept;
    Guard try_lock() noexcept;
    void unlock() noexcept;
};

}

Internal Design

  • Single std::atomic<std::uintptr_t> encodes state and waiter stack:

    • 0 → unlocked
    • 1 → locked, no waiters
    • ptr|1 → locked, waiter stack head
    • Waiters form an intrusive LIFO list on coroutine frames.
    • Handoff enqueues the next coroutine on the runtime queue.

Performance

Scenario Latency (uncontended) Notes
Lock/Unlock (no contention) ~30–50 ns Single CAS
Handoff with contention ~120–150 ns CAS + enqueue

Summary

Use AsyncMutex for exclusive access in coroutine-heavy code without blocking threads.


AsyncSemaphore

A coroutine-friendly counting semaphore controlling access to a fixed number of permits.

Overview

Provides bounded parallelism: only N coroutines can proceed concurrently; the rest suspend and are resumed through the event loop.

Features

  • co_await sem.acquire() for permit acquisition.
  • try_acquire() non-suspending fast path.
  • release(k) wakes waiters or returns permits to the counter.
  • No heap allocations on the waiting path.

Example

#include "uvent/sync/AsyncSemaphore.h"
#include "uvent/Uvent.h"
#include "uvent/system/SystemContext.h"

using namespace usub::uvent;
using usub::uvent::sync::AsyncSemaphore;

static AsyncSemaphore g_sem{2};

task::Awaitable<void> task_fn(int id)
{
    co_await g_sem.acquire();
    std::cout << "task " << id << " in\n";
    co_await system::this_coroutine::sleep_for(std::chrono::milliseconds(300));
    std::cout << "task " << id << " out\n";
    g_sem.release();
    co_return;
}

API Reference

namespace usub::uvent::sync {

class AsyncSemaphore {
public:
    explicit AsyncSemaphore(int32_t initial) noexcept;

    struct AcquireAwaiter {
        bool await_ready() noexcept;
        bool await_suspend(std::coroutine_handle<> h) noexcept;
        void await_resume() noexcept;
    };

    AcquireAwaiter acquire() noexcept;
    bool try_acquire() noexcept;
    void release(int32_t count = 1) noexcept;
};

}

Internal Design

  • Atomic count plus intrusive waiter stack.
  • Acquire fast path decrements count with CAS; otherwise enqueues waiter.
  • Release pops waiter (handoff) or increments count if none present.

Performance

Scenario Latency (permit available) Notes
Acquire/Release ~30–60 ns Single CAS
Wake waiter ~120–160 ns CAS + enqueue

Summary

Use AsyncSemaphore to cap concurrency for I/O, pools, or CPU-bound sections.


AsyncEvent

Coroutine-aware event with Auto (wake one) and Manual (wake all) reset modes.

Overview

Waiters suspend on wait(). set() wakes one or all waiters depending on the mode. Manual mode stays signaled until reset().

Features

  • Reset::Auto behaves like a futex wake-one.
  • Reset::Manual behaves like a broadcast barrier.
  • wait() is an awaitable; no thread blocking.

Example

#include "uvent/sync/AsyncEvent.h"
#include "uvent/Uvent.h"
#include "uvent/system/SystemContext.h"

using namespace usub::uvent;
using usub::uvent::sync::AsyncEvent;
using usub::uvent::sync::Reset;

static AsyncEvent g_evt{Reset::Manual, false};

task::Awaitable<void> waiter(int id)
{
    std::cout << "waiter " << id << " waiting\n";
    co_await g_evt.wait();
    std::cout << "waiter " << id << " woke\n";
    co_return;
}

task::Awaitable<void> trigger()
{
    co_await system::this_coroutine::sleep_for(std::chrono::seconds(1));
    g_evt.set();
    co_return;
}

API Reference

namespace usub::uvent::sync {

enum class Reset { Auto, Manual };

class AsyncEvent {
public:
    explicit AsyncEvent(Reset mode = Reset::Auto, bool set = false) noexcept;

    struct WaitAwaiter {
        bool await_ready() noexcept;
        bool await_suspend(std::coroutine_handle<> h) noexcept;
        void await_resume() noexcept;
    };

    WaitAwaiter wait() noexcept;
    void set() noexcept;
    void reset() noexcept;
};

}

Internal Design

  • Atomic set flag plus intrusive waiter stack.
  • Auto-reset: set() wakes a single waiter and clears the flag.
  • Manual-reset: set() wakes all waiters and keeps the flag set.

Performance

Scenario Latency Notes
Wait ready ~10–20 ns Flag read/CAS
set() wake1 ~100–140 ns Pop + enqueue
set() wakeN O(N) Linear resume cost

Summary

Use AsyncEvent for signaling readiness or state transitions between coroutines.


WaitGroup

A barrier primitive to wait for a group of coroutines to finish, similar to Go’s sync.WaitGroup.

Overview

Call add(N) before spawning N tasks. Each task calls done() once when finished. Await wait() to resume when the internal counter reaches zero.

Features

  • Zero blocking; wait() suspends the awaiting coroutine.
  • Multiple concurrent waiters supported.
  • No allocations; intrusive waiter list.

Example

#include "uvent/sync/WaitGroup.h"
#include "uvent/Uvent.h"
#include "uvent/system/SystemContext.h"

using namespace usub::uvent;
using usub::uvent::sync::WaitGroup;

static WaitGroup g_wg;

task::Awaitable<void> unit(int id)
{
    co_await system::this_coroutine::sleep_for(std::chrono::milliseconds(200));
    g_wg.done();
    co_return;
}

task::Awaitable<void> controller()
{
    g_wg.add(3);
    system::co_spawn(unit(1));
    system::co_spawn(unit(2));
    system::co_spawn(unit(3));
    co_await g_wg.wait();
    co_return;
}

API Reference

namespace usub::uvent::sync {

class WaitGroup {
public:
    void add(int count) noexcept;
    void done() noexcept;

    struct Awaiter {
        bool await_ready() noexcept;
        bool await_suspend(std::coroutine_handle<> h) noexcept;
        void await_resume() noexcept;
    };

    Awaiter wait() noexcept;
};

}

Internal Design

  • Atomic counter plus waiter stack.
  • done() decrements; when it hits zero, all waiters are resumed.

Performance

Scenario Latency Notes
add/done ~10–25 ns Atomic inc/dec
wake waiters O(N) Resume each

Summary

Use WaitGroup to join batches of coroutines without building ad-hoc barriers.


CancellationSource / CancellationToken

Lightweight cooperative cancellation for coroutines.

Overview

CancellationSource emits cancellation; CancellationToken is passed to tasks. Tasks periodically check stop_requested() or await on_cancel() to react.

Features

  • Zero blocking; cancellation is cooperative.
  • Any number of coroutines can share the same token.
  • Immediate wake of all on_cancel() awaiters.

Example

#include "uvent/sync/Cancellation.h"
#include "uvent/Uvent.h"
#include "uvent/system/SystemContext.h"

using namespace usub::uvent;
using usub::uvent::sync::CancellationSource;
using usub::uvent::sync::CancellationToken;

static CancellationSource g_src;

task::Awaitable<void> cancellable(CancellationToken tok)
{
    while (!tok.stop_requested())
        co_await system::this_coroutine::sleep_for(std::chrono::milliseconds(200));
    co_return;
}

task::Awaitable<void> demo_cancel()
{
    auto tok = g_src.token();
    system::co_spawn(cancellable(tok));
    co_await system::this_coroutine::sleep_for(std::chrono::milliseconds(1500));
    g_src.request_cancel();
    co_return;
}

API Reference

namespace usub::uvent::sync {

class CancellationToken {
public:
    bool stop_requested() const noexcept;

    struct Awaiter {
        bool await_ready() noexcept;
        bool await_suspend(std::coroutine_handle<> h) noexcept;
        void await_resume() noexcept;
    };

    Awaiter on_cancel() const noexcept;
};

class CancellationSource {
public:
    CancellationToken token() noexcept;
    void request_cancel() noexcept;
};

}

Internal Design

  • Atomic requested flag with intrusive waiter list.
  • request_cancel() flips the flag and resumes all registered waiters at once.

Performance

Scenario Latency Notes
stop_requested() ~5–10 ns Atomic load
request_cancel() O(N) Resume waiters

Summary

Use cancellation to terminate long-running coroutines, enforce deadlines, or compose with_timeout()-style utilities.