access_cell/lib.rs
1//! A cell that allows mutable access with re-entrancy support.
2//!
3//! When you call [`access`](AccessCell::access) with a closure that itself calls `access`
4//! on the same cell, those nested calls are queued and run after the current closure
5//! finishes, avoiding deadlock.
6//!
7//! This type is thread-safe: it implements [`Send`] and [`Sync`] when `T: Send`.
8
9use std::{
10 collections::VecDeque,
11 sync::{
12 atomic::{AtomicBool, Ordering},
13 Mutex, RwLock,
14 },
15};
16
17/// A cell holding a value of type `T` with re-entrant mutable access.
18///
19/// The first caller of [`access`](AccessCell::access) runs immediately; any further
20/// calls to `access` from within that closure (or its queue) are enqueued and
21/// executed in order after the current work completes.
22///
23/// Thread-safe: safe to share across threads and use from multiple threads.
24pub struct AccessCell<T> {
25 value: RwLock<T>,
26 running: AtomicBool,
27 queue: Mutex<VecDeque<Box<dyn FnOnce(&mut T) + Send>>>,
28}
29
30// Safe: only one thread holds the write guard at a time (enforced by `running` + queue).
31unsafe impl<T: Send> Send for AccessCell<T> {}
32unsafe impl<T: Send> Sync for AccessCell<T> {}
33
34impl<T> AccessCell<T> {
35 /// Creates a new `AccessCell` wrapping `value`.
36 pub fn new(value: T) -> Self {
37 Self {
38 value: RwLock::new(value),
39 running: AtomicBool::new(false),
40 queue: Mutex::new(VecDeque::new()),
41 }
42 }
43
44 /// Runs `f` with exclusive mutable access to the inner value.
45 ///
46 /// If called while another `access` closure is running (re-entrant or from
47 /// another thread), `f` is queued and run after the current closure and
48 /// any other queued closures finish.
49 ///
50 /// The closure must be `Send` so it can be moved into the queue when called
51 /// from another thread.
52 pub fn access(&self, f: impl FnOnce(&mut T) + Send + 'static) {
53 // If already running (this thread re-entrant or another thread), enqueue.
54 if self.running.swap(true, Ordering::Acquire) {
55 self.queue.lock().unwrap().push_back(Box::new(f));
56 return;
57 }
58
59 // We are the executor: hold the write lock for the whole run + drain.
60 let mut guard = self.value.write().unwrap();
61 f(&mut *guard);
62
63 while let Some(job) = self.queue.lock().unwrap().pop_front() {
64 job(&mut *guard);
65 }
66
67 drop(guard);
68 self.running.store(false, Ordering::Release);
69 }
70
71 /// Returns an immutable reference to the inner value.
72 ///
73 /// Blocks if an `access` closure is currently running. Safe to call from
74 /// any thread.
75 pub fn access_ref(&self) -> std::sync::RwLockReadGuard<'_, T> {
76 self.value.read().unwrap()
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use std::sync::Arc;
83 use std::thread;
84
85 #[test]
86 fn test_access() {
87 let value = Arc::new(crate::AccessCell::new(0));
88
89 value.access({
90 let value = value.clone();
91
92 move |_| {
93 // normally this would cause a deadlock
94 value.access(|v| {
95 *v = 10;
96 })
97 }
98 });
99
100 assert_eq!(*value.access_ref(), 10);
101 }
102
103 #[test]
104 fn test_access_multithreaded() {
105 let cell = Arc::new(crate::AccessCell::new(0i32));
106 let num_threads = 8;
107 let increments_per_thread = 100;
108
109 let handles: Vec<_> = (0..num_threads)
110 .map(|_| {
111 let cell = Arc::clone(&cell);
112 thread::spawn(move || {
113 for _ in 0..increments_per_thread {
114 cell.access(|v| *v += 1);
115 }
116 })
117 })
118 .collect();
119
120 for h in handles {
121 h.join().unwrap();
122 }
123
124 let expected = num_threads * increments_per_thread;
125 assert_eq!(*cell.access_ref(), expected);
126 }
127}