Skip to content

Add async::exclusive_access for Resource Coordination #22

@kammce

Description

@kammce

Motivation

When implementing async drivers for shared hardware resources (I2C, SPI, UART, etc.), we need a way to:

  1. Ensure only one coroutine can access the resource at a time (mutual exclusion)
  2. Block waiting coroutines with appropriate state information for scheduler optimization
  3. Allow schedulers to implement advanced features like priority inheritance and efficient wake-up queues
  4. 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::context currently owns a resource (as std::uintptr_t to 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 async

Usage 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

  1. std::uintptr_t for ownership: Stores the context's address as an integer to avoid creating reference leaks (which would violate your clang-tidy checks)

  2. 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.

  3. [[nodiscard]] attribute: Forces users to capture the guard, preventing the common mistake of calling grant() without storing the result (which would immediately release).

  4. blocked_by::external: Uses the existing blocking state, with the exclusive_access object's address stored in the block info for scheduler coordination.

  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions