-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Motivation
When implementing async drivers for shared hardware resources (I2C, SPI, UART, etc.), we need a way to:
- Ensure only one coroutine can access the resource at a time (mutual exclusion)
- Block waiting coroutines with appropriate state information for scheduler optimization
- Allow schedulers to implement advanced features like priority inheritance and efficient wake-up queues
- Provide a safe, RAII-based API that prevents resource leaks
Currently, driver implementers must manually manage ownership tracking and blocking coordination, which is error-prone and leads to boilerplate code.
Proposed Solution
Introduce async::exclusive_access - a lightweight primitive that:
- Tracks which
async::contextcurrently owns a resource (asstd::uintptr_tto avoid reference leaks) - Provides RAII guard for automatic cleanup
- Exposes ownership information for scheduler coordination
- Uses standard blocking states (
blocked_by::external)
API Design
namespace async {
class exclusive_access {
public:
class guard {
public:
explicit guard(exclusive_access& parent);
~guard(); // Automatically releases access
// Non-copyable, non-movable (must live on stack)
guard(guard const&) = delete;
guard& operator=(guard const&) = delete;
guard(guard&&) = delete;
guard& operator=(guard&&) = delete;
};
/// Grant exclusive access to a context, returns RAII guard
[[nodiscard]] guard grant(context& ctx);
/// Check if currently held by any context
bool is_held() const;
/// Convenience operator for checking if held
explicit operator bool() const;
/// Get the address of the owning context (for scheduler use)
/// Returns 0 if not held
std::uintptr_t owner() const;
private:
friend class guard;
void release();
std::uintptr_t m_owner = 0;
};
} // namespace asyncUsage Example
class my_i2c : public hal::async_i2c {
public:
async::future<void> driver_transaction(
async::context& p_context,
hal::byte p_address,
std::span<hal::byte const> p_data_out,
std::span<hal::byte> p_data_in)
{
// Wait until resource is available
while (m_bus.is_held()) { // or just: while (m_bus)
co_await block_by_external(std::bit_cast<std::uintptr_t>(&m_bus));
}
// Acquire exclusive access (RAII guard)
auto access = m_bus.grant(p_context);
// Perform I2C transaction
perform_i2c_setup(p_address);
setup_interrupt();
i2c_start();
while (i2c_bus_busy()) {
co_await block_by_io(); // Different blocking reason
}
// access automatically released when scope exits
}
private:
async::exclusive_access m_bus;
};Implementation Notes
-
std::uintptr_tfor ownership: Stores the context's address as an integer to avoid creating reference leaks (which would violate your clang-tidy checks) -
RAII guard: The guard type should be non-movable to ensure it stays on the stack and gets destroyed at scope exit. This prevents accidental transfer of ownership or early release.
-
[[nodiscard]]attribute: Forces users to capture the guard, preventing the common mistake of callinggrant()without storing the result (which would immediately release). -
blocked_by::external: Uses the existing blocking state, with theexclusive_accessobject's address stored in the block info for scheduler coordination. -
Simple scheduler compatibility: Schedulers that don't need advanced coordination can simply ignore the ownership information and use round-robin scheduling.
Benefits
- Safety: RAII prevents resource leaks
- Clarity: Clear API makes driver code readable
- Flexibility: Simple schedulers can ignore it, sophisticated schedulers leverage it
- Performance: Enables efficient scheduler wake-up queues and priority inheritance
- No overhead: Zero-cost abstraction for basic use cases