HN
Today

Shared mutable state in Rust (2022)

This deep dive demystifies managing shared mutable state in Rust, outlining essential patterns with Arc and Mutex for both sync and async operations. It highlights critical pitfalls, particularly MutexGuard behavior with await, and provides robust solutions including wrapper structs and various alternatives. A must-read for Rust developers aiming for safe, performant concurrency without the usual headaches.

3
Score
0
Comments
#15
Highest Rank
7h
on Front Page
First Seen
Apr 5, 12:00 PM
Last Seen
Apr 5, 6:00 PM
Rank Over Time
19161520222629

The Lowdown

Alice Ryhl's 2022 article comprehensively explains how to manage shared mutable state in Rust, a common necessity in concurrent applications. It details the use of standard library primitives like Arc and Mutex while also exploring common pitfalls and advanced alternatives for both synchronous and asynchronous contexts.

  • Threaded Sharing Foundation: To share a mutable value across multiple threads, the article recommends pairing an Arc for shared ownership with a Mutex for exclusive mutable access. Arc handles reference counting for safe memory management, while Mutex prevents data races by ensuring only one thread can modify the value at a time.
  • The Wrapper Pattern: A key recommendation is to encapsulate Arc<Mutex<T>> within a custom wrapper struct. This design choice reduces code clutter, hides implementation details (making it easier to swap synchronization primitives like RwLock), and helps prevent accidentally holding locks for too long, especially in async code.
  • Asynchronous Concurrency Caveats: The article issues a critical warning: "You cannot .await anything while a mutex is locked." It explains how await points allow the runtime to swap tasks, potentially leading to deadlocks if a MutexGuard is held. The compiler often catches this via Send trait requirements, but not always (e.g., spawn_local, or Send locks like dashmap's guards).
  • Preventing Async Deadlocks: To mitigate async locking issues, the author advises creating non-async helper methods on the wrapper struct that handle the mutex locking. This ensures that no .await calls can occur while the mutex is held, preventing deadlocks.
  • Tokio's Asynchronous Locks: It differentiates std::sync::Mutex (blocking) from tokio::sync::Mutex (asynchronous). Tokio's mutexes allow .await calls while locked by yielding control, but they are generally slower than blocking mutexes and should only be used when necessary.
  • Alternative Synchronization Primitives: The article explores several alternatives to a basic Mutex, including RwLock (with a warning about writer starvation, preferring parking_lot's fair RwLock), arc-swap for rarely updated values (potentially combined with im for copy-on-write), evmap for eventually consistent maps, and dashmap for sharded concurrent hash maps. It also mentions thread_local for per-thread copies and std::sync::atomic for simple integer types.
  • Handling References: Directly returning references to data within a mutex is problematic because the reference's lifetime is tied to the MutexGuard. Suggested workarounds include cloning the value (especially for async operations) or employing a with_* pattern, which accepts a closure to operate on the locked data.
  • Avoiding I/O in Mutexes: For I/O resources like TcpStream, the article strongly advises against placing them directly within a shared mutex. Instead, it promotes the actor pattern, where each I/O resource is owned by a dedicated task, and actor handles are shared.

By understanding these patterns, caveats, and alternatives, Rust developers can effectively manage shared mutable state, building robust, performant, and data-race-free concurrent applications, a cornerstone of Rust's safety guarantees.