async-lock

repository·master·Indexed 43 days ago

https://github.com/smol-rs/async-lock

A Rust crate providing asynchronous synchronization primitives including Mutex, RwLock, Semaphore, Barrier, and OnceCell. Designed for no_std environments and for holding locks across .await points safely to prevent deadlocks in async applications. Version 3.4.2.

Tokens
7.5K
Snippets
30
Records
33
Agent score
54%

What's inside async-lock

  1. Overview of async-lock synchronization primitives

    master

    The async-lock crate provides a set of asynchronous synchronization primitives designed for use in async Rust environments. It includes tools for managing mutual exclusion, shared access, and task synchronization.

    Available primitives:

    • Barrier: Synchronizes multiple tasks so they all reach a certain point before proceeding.
    • Mutex: Provides mutual exclusion, ensuring only one task can access data at a time.
    • OnceCell: A memory location that can be written to exactly once.
    • RwLock: A reader-writer lock that allows multiple concurrent readers or a single exclusive writer.
    • Semaphore: Limits the number of concurrent operations allowed to proceed.
  2. Available async synchronization primitives in async-lock

    master

    The async-lock crate provides the following core synchronization primitives:

    • Barrier: Enables tasks to synchronize all together at the same time.
    • Mutex: A mutual exclusion lock.
    • OnceCell: A memory location that can be written to at most once.
    • RwLock: A reader-writer lock, allowing any number of readers or a single writer.
    • Semaphore: Limits the number of concurrent operations.
  3. When to use async-lock vs std::sync

    master

    In most cases, you should prefer std::sync types over async-lock because they are optimized for the operating system and generally faster.

    Use async-lock only in these two specific scenarios:

    1. no_std environments: When you are working in an environment without the standard library.
    2. Holding locks across .await points: If you hold an std::sync lock guard across an .await, your future will become non-Send and you are highly likely to cause deadlocks. async-lock primitives are designed to be held across await points safely.

    Note on performance: If the std feature is enabled, async-lock uses std::sync::Mutex for its notification system. If not enabled, it falls back to a significantly slower strategy. Therefore, async-lock rarely provides a performance win over std::sync unless the use cases above apply.

  4. Use upgradable read locks to transition from reader to writer

    master

    An upgradable read lock allows a single reader to reserve the right to upgrade to a write lock. There can be at most one upgradable read lock held at a time.

    Methods for RwLock:

    • upgradable_read(): Asynchronously acquires an upgradable read lock. Returns an UpgradableRead guard.
    • try_upgradable_read(): Attempts to acquire an upgradable read lock immediately.
    • upgradable_read_blocking(): Blocks the current thread until acquired.

    Methods for RwLockUpgradableReadGuard:

    • upgrade(guard): Asynchronously upgrades the guard to a write lock. Returns an Upgrade future.
    • try_upgrade(guard): Attempts to upgrade immediately. Returns Result<RwLockWriteGuard, Self>.
    • downgrade(guard): Downgrades the upgradable lock into a regular RwLockReadGuard.
    • upgrade_blocking(guard): Blocks the thread until the upgrade is complete.
    # futures_lite::future::block_on(async {
    use async_lock::{RwLock, RwLockUpgradableReadGuard};
    
    let lock = RwLock::new(1);
    let reader = lock.upgradable_read().await;
    assert_eq!(*reader, 1);
    assert_eq!(*lock.try_read().unwrap(), 1);
    
    let mut writer = RwLockUpgradableReadGuard::upgrade(reader).await;
    *writer = 2;
    # });
  5. Use RwLock for async reader-writer locking

    master

    An async RwLock allows multiple readers or a single writer at any point in time. The locking strategy is write-preferring, meaning writers are never starved. Releasing a write lock wakes the next blocked reader and the next blocked writer.

    Common operations include:

    • new(t): Creates a new lock with value t.
    • read(): Acquires an asynchronous read lock.
    • write(): Acquires an asynchronous write lock.
    • try_read() / try_write(): Attempts to acquire a lock without waiting, returning None if unavailable.
    • into_inner(): Consumes the lock and returns the inner value.
    # futures_lite::future::block_on(async {
    use async_lock::RwLock;
    
    let lock = RwLock::new(5);
    
    // Multiple read locks can be held at a time.
    let r1 = lock.read().await;
    let r2 = lock.read().await;
    assert_eq!(*r1, 5);
    assert_eq!(*r2, 5);
    drop((r1, r2));
    
    // Only one write lock can be held at a time.
    let mut w = lock.write().await;
    *w += 1;
    assert_eq!(*w, 6);
    # });
  6. Use OnceCell for one-time initialization

    master

    A OnceCell<T> is a memory location that can be written to at most once. It is ideal for one-time initialization patterns, such as lazy loading expensive resources (e.g., database connections or network clients). Once initialized, the value can be accessed via a reference without needing an RAII guard like Mutex or RwLock.

    use async_lock::OnceCell;
    
    # struct Foobar;
    
    async fn very_expensive_initialization() -> Foobar {
        // Imagine this is very expensive to initialize
        # Foobar
    }
    
    struct LazyFoobar {
        inner: OnceCell<Foobar>,
    }
    
    impl LazyFoobar {
        fn new() -> Self {
            Self {
                inner: OnceCell::new(),
            }
        }
    
        async fn load(&self) -> &Foobar {
            self.inner.get_or_init(|| async {
                very_expensive_initialization().await
            }).await
        }
    }
  7. Use blocking methods in synchronous contexts

    master

    If you need to interact with a OnceCell from a synchronous thread (e.g., a thread spawned via std::thread::spawn) that is waiting on a value initialized in an async context, use the _blocking variants:

    • wait_blocking(): Blocks the thread until the cell is initialized.
    • get_or_init_blocking(): Blocks the thread to initialize the cell if it is not yet set.
    • get_or_try_init_blocking(): Blocks the thread to initialize the cell with a result-bearing closure.
    • set_blocking(): Blocks the thread to set the value.

    CRITICAL: Never call these methods inside an async function or an async executor, as they will block the executor thread and likely cause a deadlock.

    // Inside a synchronous thread
    let cell = std::sync::Arc::new(OnceCell::new());
    let cell_clone = cell.clone();
    
    std::thread::spawn(move || {
        // This blocks the thread until the async task calls .set()
        let val = cell_clone.wait_blocking();
        println!("Got value: {}", val);
    });
  8. Use Mutex for async mutual exclusion

    master

    The Mutex<T> provides a way to protect shared data in asynchronous contexts. It uses an eventual fairness mechanism: if a lock operation is starved for longer than 0.5 milliseconds, the mutex switches to a fairer locking strategy to prevent starvation.

    To use it, create a new mutex with Mutex::new(data), and acquire the lock using .lock().await. The lock returns a guard that automatically releases the mutex when dropped.

    # futures_lite::future::block_on(async {
    use async_lock::Mutex;
    
    let m = Mutex::new(1);
    
    let mut guard = m.lock().await;
    *guard = 2;
    
    assert!(m.try_lock().is_none());
    drop(guard);
    assert_eq!(*m.try_lock().unwrap(), 2);
    # });
  9. Initialize OnceCell with get_or_init or get_or_try_init

    master

    To initialize a OnceCell using an asynchronous closure, use get_or_init or get_or_try_init.

    • get_or_init: Takes a closure that returns a Future<Output = T>. If the cell is already initialized, it returns the existing value. If not, it runs the closure and sets the value.
    • get_or_try_init: Takes a closure that returns a Future<Output = Result<T, E>>. The cell will not be initialized if the closure returns an error, allowing for retry logic or error handling.

    Multiple tasks calling these methods concurrently will result in only one closure being invoked; others will await the result of the first initialization.

    use async_lock::OnceCell;
    
    // Using get_or_init
    let cell = OnceCell::new();
    let value = cell.get_or_init(|| async { 1 }).await;
    assert_eq!(value, &1);
    
    // Using get_or_try_init with error handling
    let cell = OnceCell::new();
    let result = cell.get_or_try_init(|| async { 
        if some_condition() { Ok(1) } else { Err("failed") }
    }).await;
  10. Check available permits

    master

    Use available_permits() to return the current number of permits available for acquisition.

    use async_lock::Semaphore;
    
    let s = Semaphore::new(2);
    assert_eq!(s.available_permits(), 2);
    
    let guard = s.try_acquire().unwrap();
    assert_eq!(s.available_permits(), 1);
  11. Manage reference-counted locks with Arc<RwLock<T>>

    master

    When using RwLock inside an Arc, use the _arc variants of the locking methods to ensure the lock guard maintains a reference to the underlying data.

    Methods for Arc<RwLock<T>>:

    • read_arc(): Asynchronously acquires a reference-counted read lock. Returns ReadArc.
    • try_read_arc(): Attempts to acquire a reference-counted read lock. Returns Option<RwLockReadGuardArc>.
    • read_arc_blocking(): Blocks the thread until a reference-counted read lock is acquired.
    • write_arc(): Asynchronously acquires a reference-counted write lock. Returns WriteArc.
    • try_write_arc(): Attempts to acquire a reference-counted write lock. Returns Option<RwLockWriteGuardArc>.
    • write_arc_blocking(): Blocks the thread until a reference-counted write lock is acquired.
    • upgradable_read_arc(): Asynchronously acquires a reference-counted upgradable read lock. Returns UpgradableReadArc.
    • try_upgradable_read_arc(): Attempts to acquire a reference-counted upgradable read lock. Returns Option<RwLockUpgradableReadGuardArc>.
    • upgradable_read_arc_blocking(): Blocks the thread until an upgradable read lock is acquired.

    Methods for RwLockUpgradableReadGuardArc:

    • upgrade(guard): Asynchronously upgrades to a write lock. Returns UpgradeArc.
    • try_upgrade(guard): Attempts to upgrade immediately. Returns Result<RwLockWriteGuardArc, Self>.
    • downgrade(guard): Downgrades to a RwLockReadGuardArc.
    # futures_lite::future::block_on(async {
    use std::sync::Arc;
    use async_lock::{RwLock, RwLockUpgradableReadGuardArc};
    
    let lock = Arc::new(RwLock::new(1));
    
    let reader = lock.upgradable_read_arc().await;
    assert_eq!(*reader, 1);
    assert!(lock.try_read_arc().is_some());
    
    let mut writer = RwLockUpgradableReadGuardArc::upgrade(reader).await;
    *writer = 2;
    # });
  12. Synchronize tasks asynchronously with `Barrier::wait()`

    master

    To synchronize tasks in an asynchronous context, call wait(). This returns a BarrierWait future that resolves when the barrier is reached.

    Barriers are reusable: once all tasks have synchronized, the barrier resets and can be used for the next synchronization point.

    use async_lock::Barrier;
    use futures_lite::future;
    use std::sync::Arc;
    use std::thread;
    
    let barrier = Arc::new(Barrier::new(5));
    
    for _ in 0..5 {
        let b = barrier.clone();
        thread::spawn(move || {
            future::block_on(async {
                println!("before wait");
                b.wait().await;
                println!("after wait");
            });
        });
    }