seize/collector.rs
1use crate::raw::{self, membarrier, Thread};
2use crate::{LocalGuard, OwnedGuard};
3
4use std::fmt;
5use std::sync::OnceLock;
6
7/// A concurrent garbage collector.
8///
9/// A `Collector` manages the access and retirement of concurrent objects
10/// Objects can be safely loaded through *guards*, which can be created using
11/// the [`enter`](Collector::enter) or [`enter_owned`](Collector::enter_owned)
12/// methods.
13///
14/// Every instance of a concurrent data structure should typically own its
15/// `Collector`. This allows the garbage collection of non-`'static` values, as
16/// memory reclamation is guaranteed to run when the `Collector` is dropped.
17#[repr(transparent)]
18pub struct Collector {
19 /// The underlying raw collector instance.
20 pub(crate) raw: raw::Collector,
21}
22
23impl Default for Collector {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl Collector {
30 /// The default batch size for a new collector.
31 const DEFAULT_BATCH_SIZE: usize = 32;
32
33 /// Creates a new collector.
34 pub fn new() -> Self {
35 // Initialize the `membarrier` module, detecting the presence of
36 // operating-system strong barrier APIs.
37 membarrier::detect();
38
39 // available_parallelism is quite slow (microseconds).
40 static CPUS: OnceLock<usize> = OnceLock::new();
41 let cpus = *CPUS.get_or_init(|| {
42 std::thread::available_parallelism()
43 .map(Into::into)
44 .unwrap_or(1)
45 });
46
47 // Ensure every batch accumulates at least as many entries
48 // as there are threads on the system.
49 let batch_size = cpus.max(Self::DEFAULT_BATCH_SIZE);
50
51 Self {
52 raw: raw::Collector::new(cpus, batch_size),
53 }
54 }
55
56 /// Sets the number of objects that must be in a batch before reclamation is
57 /// attempted.
58 ///
59 /// Retired objects are added to thread-local *batches* before starting the
60 /// reclamation process. After `batch_size` is hit, the objects are moved to
61 /// separate *retirement lists*, where reference counting kicks in and
62 /// batches are eventually reclaimed.
63 ///
64 /// A larger batch size amortizes the cost of retirement. However,
65 /// reclamation latency can also grow due to the large number of objects
66 /// needed to be freed. Note that reclamation can not be attempted
67 /// unless the batch contains at least as many objects as the number of
68 /// active threads.
69 ///
70 /// The default batch size is `32`.
71 pub fn batch_size(mut self, batch_size: usize) -> Self {
72 self.raw.batch_size = batch_size;
73 self
74 }
75
76 /// Marks the current thread as active, returning a guard that protects
77 /// loads of concurrent objects for its lifetime. The thread will be
78 /// marked as inactive when the guard is dropped.
79 ///
80 /// Note that loads of objects that may be retired must be protected with
81 /// the [`Guard::protect`]. See [the
82 /// guide](crate::guide#starting-operations) for an introduction to
83 /// using guards, or the documentation of [`LocalGuard`] for
84 /// more details.
85 ///
86 /// Note that `enter` is reentrant, and it is legal to create multiple
87 /// guards on the same thread. The thread will stay marked as active
88 /// until the last guard is dropped.
89 ///
90 /// [`Guard::protect`]: crate::Guard::protect
91 ///
92 /// # Performance
93 ///
94 /// Performance-wise, creating and destroying a `LocalGuard` is about the
95 /// same as locking and unlocking an uncontended `Mutex`. Because of
96 /// this, guards should be reused across multiple operations if
97 /// possible. However, holding a guard prevents the reclamation of any
98 /// concurrent objects retired during its lifetime, so there is
99 /// a tradeoff between performance and memory usage.
100 ///
101 /// # Examples
102 ///
103 /// ```rust
104 /// # use std::sync::atomic::{AtomicPtr, Ordering};
105 /// use seize::Guard;
106 /// # let collector = seize::Collector::new();
107 ///
108 /// // An atomic object.
109 /// let ptr = AtomicPtr::new(Box::into_raw(Box::new(1_usize)));
110 ///
111 /// {
112 /// // Create a guard that is active for this scope.
113 /// let guard = collector.enter();
114 ///
115 /// // Read the object using a protected load.
116 /// let value = guard.protect(&ptr, Ordering::Acquire);
117 /// unsafe { assert_eq!(*value, 1) }
118 ///
119 /// // If there are other thread that may retire the object,
120 /// // the pointer is no longer valid after the guard is dropped.
121 /// drop(guard);
122 /// }
123 /// # unsafe { drop(Box::from_raw(ptr.load(Ordering::Relaxed))) };
124 /// ```
125 #[inline]
126 pub fn enter(&self) -> LocalGuard<'_> {
127 LocalGuard::enter(self)
128 }
129
130 /// Create an owned guard that protects objects for its lifetime.
131 ///
132 /// Unlike local guards created with [`enter`](Collector::enter), owned
133 /// guards are independent of the current thread, allowing them to
134 /// implement `Send` and `Sync`. See the documentation of [`OwnedGuard`]
135 /// for more details.
136 #[inline]
137 pub fn enter_owned(&self) -> OwnedGuard<'_> {
138 OwnedGuard::enter(self)
139 }
140
141 /// Retires a value, running `reclaim` when no threads hold a reference to
142 /// it.
143 ///
144 /// Note that this method is disconnected from any guards on the current
145 /// thread, so the pointer may be reclaimed immediately. Use
146 /// [`Guard::defer_retire`](crate::Guard::defer_retire) if the pointer may
147 /// still be accessed by the current thread while the guard is active.
148 ///
149 /// # Safety
150 ///
151 /// The retired pointer must no longer be accessible to any thread that
152 /// enters after it is removed. It also cannot be accessed by the
153 /// current thread after `retire` is called.
154 ///
155 /// Additionally, the pointer must be valid to pass to the provided
156 /// reclaimer, once it is safe to reclaim.
157 ///
158 /// # Examples
159 ///
160 /// Common reclaimers are provided by the [`reclaim`](crate::reclaim)
161 /// module.
162 ///
163 /// ```
164 /// # use std::sync::atomic::{AtomicPtr, Ordering};
165 /// # let collector = seize::Collector::new();
166 /// use seize::reclaim;
167 ///
168 /// // An atomic object.
169 /// let ptr = AtomicPtr::new(Box::into_raw(Box::new(1_usize)));
170 ///
171 /// // Create a guard.
172 /// let guard = collector.enter();
173 ///
174 /// // Store a new value.
175 /// let old = ptr.swap(Box::into_raw(Box::new(2_usize)), Ordering::Release);
176 ///
177 /// // Reclaim the old value.
178 /// //
179 /// // Safety: The `swap` above made the old value unreachable for any new threads.
180 /// // Additionally, the old value was allocated with a `Box`, so `reclaim::boxed`
181 /// // is valid.
182 /// unsafe { collector.retire(old, reclaim::boxed) };
183 /// # unsafe { collector.retire(ptr.load(Ordering::Relaxed), reclaim::boxed) };
184 /// ```
185 ///
186 /// Alternative, a custom reclaimer function can be used.
187 ///
188 /// ```
189 /// use seize::Collector;
190 ///
191 /// let collector = Collector::new();
192 ///
193 /// // Allocate a value and immediately retire it.
194 /// let value: *mut usize = Box::into_raw(Box::new(1_usize));
195 ///
196 /// // Safety: The value was never shared.
197 /// unsafe {
198 /// collector.retire(value, |ptr: *mut usize, _collector: &Collector| unsafe {
199 /// // Safety: The value was allocated with `Box::new`.
200 /// let value = Box::from_raw(ptr);
201 /// println!("Dropping {value}");
202 /// drop(value);
203 /// });
204 /// }
205 /// ```
206 #[inline]
207 pub unsafe fn retire<T>(&self, ptr: *mut T, reclaim: unsafe fn(*mut T, &Collector)) {
208 debug_assert!(!ptr.is_null(), "attempted to retire a null pointer");
209
210 // Note that `add` doesn't ever actually reclaim the pointer immediately if
211 // the current thread is active. Instead, it adds it to the current thread's
212 // reclamation list, but we don't guarantee that publicly.
213 unsafe { self.raw.add(ptr, reclaim, Thread::current()) }
214 }
215
216 /// Reclaim any values that have been retired.
217 ///
218 /// This method reclaims any objects that have been retired across *all*
219 /// threads. After calling this method, any values that were previous
220 /// retired, or retired recursively on the current thread during this
221 /// call, will have been reclaimed.
222 ///
223 /// # Safety
224 ///
225 /// This function is **extremely unsafe** to call. It is only sound when no
226 /// threads are currently active, whether accessing values that have
227 /// been retired or accessing the collector through any type of guard.
228 /// This is akin to having a unique reference to the collector. However,
229 /// this method takes a shared reference, as reclaimers to
230 /// be run by this thread are allowed to access the collector recursively.
231 ///
232 /// # Notes
233 ///
234 /// Note that if reclaimers initialize guards across threads, or initialize
235 /// owned guards, objects retired through those guards may not be
236 /// reclaimed.
237 pub unsafe fn reclaim_all(&self) {
238 unsafe { self.raw.reclaim_all() };
239 }
240
241 // Create a reference to `Collector` from an underlying `raw::Collector`.
242 pub(crate) fn from_raw(raw: &raw::Collector) -> &Collector {
243 unsafe { &*(raw as *const raw::Collector as *const Collector) }
244 }
245}
246
247impl Eq for Collector {}
248
249impl PartialEq for Collector {
250 /// Checks if both references point to the same collector.
251 #[inline]
252 fn eq(&self, other: &Self) -> bool {
253 self.raw.id == other.raw.id
254 }
255}
256
257impl fmt::Debug for Collector {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 f.debug_struct("Collector")
260 .field("batch_size", &self.raw.batch_size)
261 .finish()
262 }
263}