tracing_rc/sync/mod.rs
1use std::{
2 mem::ManuallyDrop,
3 ops::Deref,
4 sync::{
5 Arc,
6 Weak,
7 },
8};
9
10use atomic::Atomic;
11use bytemuck::NoUninit;
12use parking_lot::{
13 RwLock,
14 RwLockReadGuard,
15};
16
17/// Notes:
18/// There are several moving parts the collector needs to care about in order to prevent leaks or
19/// early drop.
20///
21/// 1. The reference count of traced objects. This may change at any time.
22/// 2. The graph of child components. This may be altered any time a reference is access outside the
23/// collector.
24/// 3. The status of the object. This may change at any time.
25///
26/// If we decide an object is live, it is pretty safe to mark all of its children live since we
27/// don't do anything clever with buffering. The logic here is that the collector takes out a strong
28/// reference to the objects it traces, so any concurrent drops of objects during collection (making
29/// them dead) will put a weak reference into the young gen. If the object is acyclical, the [`Arc`]
30/// drop implementation will call drop on [`AtomicInner`] which will tear down its internal data
31/// properly. If the object is a node in a cyclical graph, it will retain at least one strong count
32/// which will allow the collector to upgrade its weak pointer and trace/collect the object.
33///
34/// If we decide an object is dead, we need to exercise caution when tearing down its data, since
35/// the child graph may have changed between the time we traced it and the time we're checking
36/// refcounts. If we know the child graph is unchanged, we can follow the normal rules for child
37/// cleanup (cleanup all children whose refcounts come from the dead graph). If it has changed, it's
38/// pretty safe to treat the object as still live, since _someone_ must have had a (possibly
39/// transitive) strong reference to it in order to alter the child graph, and so we'll at least have
40/// a weak reference in the old/young gen if it's been dropped since.
41///
42/// We track the dirty state of the child graph using [`Status::Traced`], which the collector sets
43/// as the state of any node it reaches during tracing. All operations which expose references
44/// overwrite this status (we can't rely on `&mut` due to interior mutability). This way, the
45/// collector can know that if it sees a dead object with the `Traced` status, both that object and
46/// its children are possibly dead. Conversely, if `Traced` has been overwritten, the object was
47/// definitely still alive.
48///
49/// In order to prevent races with altering child graphs during tracing, the collector acquires an
50/// exclusive lock prior to setting `Status::Traced`. This is to prevent races where a thread
51/// acquires a reference to an object's data prior to its traced status being set and subsequently
52/// modifies the graph through that reference after it's been set.
53mod collector;
54
55/// Contains the `sync` version of the [`Trace`] trait.
56pub mod trace;
57
58pub use collector::{
59 collect,
60 collect_full,
61 collect_with_options,
62 count_roots,
63 GcVisitor,
64};
65use collector::{
66 WeakNode,
67 YOUNG_GEN,
68};
69#[doc(inline)]
70pub use trace::Trace;
71
72/// Wraps a shared reference to a value in a [`Agc`].
73pub struct Ref<'a, T: ?Sized> {
74 guard: RwLockReadGuard<'a, ManuallyDrop<T>>,
75}
76
77impl<T: ?Sized> Deref for Ref<'_, T> {
78 type Target = T;
79
80 #[inline]
81 fn deref(&self) -> &T {
82 self.guard.deref().deref()
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, NoUninit)]
87#[repr(i8)]
88enum Status {
89 /// The node has had its refcount incremented recently or a mutable/immutable borrow checked
90 /// out and is definitely alive.
91 Live,
92 /// The node had its reference count decremented recently and may be dead.
93 RecentlyDecremented,
94 /// The Collector visited this node during tracing, and the node has not been accessed since.
95 /// This state must be invalidated by any operation which returns a references (mutable or
96 /// otherwise), or the collector may prematurely destroy data.
97 ///
98 /// The particular area of concern is a graph that looks like:
99 /// - `Stack -> (Candidate) A -> B`
100 /// - `Thread A: Traverse A -> B`
101 /// - `Collector: Trace A, B`
102 /// - `Thread A: Attach B <- C, release A`
103 /// - `{ A (dead) \\ B <- C (Stack) }`
104 /// - `Collector: Check A, A is dead (strong = 1, count = 1) & B is dead (strong = 2, count =
105 /// 2).`
106 ///
107 /// If we don't know that A was possibly modified (and may have had its child graph modified),
108 /// we'll try to destroy B. By invalidating `Traced` on access to A's data, we know to remove B
109 /// from the list of `Dead` nodes.
110 Traced,
111 /// The Collector completed collection and believes the node is dead. This status is
112 /// unrecoverable.
113 Dead,
114}
115
116/// A cycle-collected reference-counted smart pointer which may be shared across threads and
117/// supports concurrent collection.
118///
119/// `Agc<T>` provides shared ownership of a value of type `T`, allocated on the heap. Cloning it
120/// will produce a new `Agc` instance which points to the same allocation as the original `Agc`.
121///
122/// Unlike [`std::sync::Arc`], `Agc` pointers may refer to each other in a way that creates cycles
123/// arbitrarily without causing leaks, provided that the program calls [`collect`] to collect those
124/// cycles.
125///
126/// - In most cases you **must** call collect to reclaim memory for dropped `Agc`s _even if they are
127/// acyclical_.
128/// - You may call [`collect`] from any thread to collect cycles. The implementation of collect is
129/// intended to provide very low pause times for concurrently running threads, although it will
130/// block the thread which actually invokes [`collect`] until the collection is complete.
131/// - While the exact details are subject to change, the current implementation will block
132/// access to `Agc`s data for as long as it takes to update its metadata, and will only do so
133/// if that `Agc` is a candidate for collection.
134pub struct Agc<T: Trace + 'static> {
135 ptr: Arc<AtomicInner<T>>,
136}
137
138impl<T> Agc<T>
139where
140 T: Trace + 'static,
141{
142 /// Construct a new `Agc` containing `data` which will be automatically cleaned up with it is no
143 /// longer reachable, even in the presence of cyclical references.
144 pub fn new(data: T) -> Self {
145 Self {
146 ptr: Arc::new(AtomicInner {
147 status: Atomic::new(Status::Live),
148 data: RwLock::new(ManuallyDrop::new(data)),
149 }),
150 }
151 }
152}
153
154impl<T> Agc<T>
155where
156 T: Trace + 'static,
157{
158 /// Retrieve the current number of strong references outstanding.
159 pub fn strong_count(this: &Self) -> usize {
160 Arc::strong_count(&this.ptr)
161 }
162
163 /// Blocks the thread until it can acquire a non-exclusive reference into this `Agc`.
164 ///
165 /// The returned object is an RAII guard which releases a shared lock. Multiple references may
166 /// be taken out at the same time.
167 ///
168 /// # Deadlocks
169 /// May deadlock if the value is `Dead`. This should not occur under normal circumstances and
170 /// likely indicates a bug in a [`Trace`] implementation.
171 ///
172 /// # Panics
173 /// Panics if the value is `Dead`. This should not occur under normal circumstances and likely
174 /// indicates a bug in a [`Trace`] implementation.
175 pub fn read(&self) -> Ref<'_, T> {
176 if self.ptr.status.load(atomic::Ordering::Acquire) != Status::Dead {
177 let guard = self.ptr.data.read_recursive();
178 // We must update this after acquiring the lock to prevent races with tracing and seeing
179 // nodes as possibly dirty.
180 self.ptr
181 .status
182 .store(Status::Live, atomic::Ordering::Release);
183 Ref { guard }
184 } else {
185 panic!("Attempted to acquire a reference to a dead object");
186 }
187 }
188
189 /// Try to get a non-exclusive reference into this `Agc`. This will not block the calling
190 /// thread. This may fail if the object is being concurrently traced by the collector.
191 ///
192 /// Returns None if the pointer is dead or exclusively referenced.
193 pub fn try_borrow(&self) -> Option<Ref<'_, T>> {
194 if self.ptr.status.load(atomic::Ordering::Acquire) != Status::Dead {
195 if let Some(guard) = self.ptr.data.try_read_recursive() {
196 // We must update this after acquiring the lock to prevent races with tracing and
197 // seeing nodes as possibly dirty.
198 self.ptr
199 .status
200 .store(Status::Live, atomic::Ordering::Release);
201 return Some(Ref { guard });
202 }
203 }
204
205 None
206 }
207
208 /// Returns true if both this & other point to the same allocation.
209 pub fn ptr_eq(this: &Self, other: &Self) -> bool {
210 Arc::ptr_eq(&this.ptr, &other.ptr)
211 }
212
213 fn node(&self) -> WeakNode {
214 let inner_ptr = Arc::downgrade(&self.ptr) as Weak<_>;
215 WeakNode { inner_ptr }
216 }
217}
218
219impl<T> Drop for Agc<T>
220where
221 T: Trace + 'static,
222{
223 fn drop(&mut self) {
224 // It is okay if this overwrites/is overwritten by Status::Traced.
225 // - It's fine if we overwrite Traced because we won't drop the inner data here (the
226 // collector holds a strong reference), so we're not worried about child destructors
227 // altering the object graph.
228 // - If the collector completes its tracing before this update, it will end up believing
229 // this object is live anyways, since we haven't dropped the strong reference yet. Since
230 // we are going to always store a weak reference in the young gen, we're not worried about
231 // losing track of the object if it needs later cleanup.
232 //
233 // We're also okay with races with Status::Dead in the case of Trace bugs.
234 // - If the collector wins and sets Status::Dead, we won't overwrite it and will just do
235 // nothing here.
236 // - If we win the race, the status will get overwritten with Dead and since we don't try to
237 // read the data stored in this pointer, we don't end up deadlocking or panicking. The
238 // leaked write lock prevents UB.
239 if self
240 .ptr
241 .status
242 .fetch_update(
243 atomic::Ordering::AcqRel,
244 atomic::Ordering::Relaxed,
245 |status| match status {
246 Status::Dead => None,
247 _ => Some(Status::RecentlyDecremented),
248 },
249 )
250 .is_err()
251 {
252 return;
253 }
254
255 let weak_node = self.node();
256 YOUNG_GEN.insert(weak_node, 0);
257 }
258}
259
260impl<T> Trace for Agc<T>
261where
262 T: Trace + 'static,
263{
264 fn visit_children(&self, visitor: &mut GcVisitor) {
265 visitor.visit_node(self)
266 }
267}
268
269impl<T> std::fmt::Debug for Agc<T>
270where
271 T: Trace + 'static,
272{
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct("Agc")
275 .field("strong", &format_args!("{}", Self::strong_count(self)))
276 .field(
277 "data",
278 &format_args!(
279 "{:?} @ {:?}",
280 self.try_borrow().map(|_| std::any::type_name::<T>()),
281 (&**self.ptr.data.read_recursive() as *const T)
282 ),
283 )
284 .finish()
285 }
286}
287
288impl<T> Clone for Agc<T>
289where
290 T: Trace + 'static,
291{
292 fn clone(&self) -> Self {
293 let _guard = self.ptr.data.read_recursive();
294 self.ptr
295 .status
296 .store(Status::Live, atomic::Ordering::Release);
297
298 Self {
299 ptr: self.ptr.clone(),
300 }
301 }
302}
303
304impl<T: Trace + 'static> PartialEq for Agc<T>
305where
306 T: PartialEq,
307{
308 fn eq(&self, other: &Self) -> bool {
309 *self.read() == *other.read()
310 }
311}
312
313impl<T: Trace + 'static> Eq for Agc<T> where T: Eq {}
314
315impl<T: Trace + 'static> PartialOrd for Agc<T>
316where
317 T: PartialOrd,
318{
319 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
320 self.read().partial_cmp(&other.read())
321 }
322}
323
324impl<T: Trace + 'static> Ord for Agc<T>
325where
326 T: Ord,
327{
328 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
329 self.read().cmp(&other.read())
330 }
331}
332
333pub(crate) struct AtomicInner<T: ?Sized + Trace> {
334 status: Atomic<Status>,
335 data: RwLock<ManuallyDrop<T>>,
336}
337
338impl<T> Drop for AtomicInner<T>
339where
340 T: ?Sized + Trace,
341{
342 fn drop(&mut self) {
343 self.drop_data();
344 }
345}
346
347impl<T> std::fmt::Debug for AtomicInner<T>
348where
349 T: ?Sized + Trace,
350{
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 f.debug_struct("Inner")
353 .field("status", &self.status)
354 .field(
355 "data",
356 &format_args!(
357 "{} @ {:?}",
358 std::any::type_name::<T>(),
359 std::ptr::addr_of!(self.data)
360 ),
361 )
362 .finish()
363 }
364}
365
366impl<T> AtomicInner<T>
367where
368 T: ?Sized + Trace,
369{
370 fn drop_data(&self) {
371 if let Some(mut data_guard) = self.data.try_write() {
372 self.status.store(Status::Dead, atomic::Ordering::Release);
373
374 unsafe { ManuallyDrop::drop(&mut data_guard) };
375
376 // This is only okay because we're using `parking_lot` mutex. If this is a
377 // `std::sync::RwLock` it will cause undefined behavior or leak.
378 //
379 // https://github.com/rust-lang/rust/issues/85434
380 std::mem::forget(data_guard);
381 }
382 }
383}