Which I had, by pure coincidence, been using as the punchline for jokes about how complex concurrent garbage collection systems since, uh, probably the same several years timespan since I last read the paper. Woops.
The basic idea with the cycle collection scheme is that by default, objects are the same as reference-counting. The difference is that when your count is decremented to a number other than 0, you add the object to a list, and then when doing a garbage collection you scan all the objects in your list (recursively visiting all objects reachable from each object) to see if any of them form graphs of objects where all the objects’ references are accounted for with only internal pointers to eachother. The idea is that in order to form a cycle, you have to have a mutator thread:
Cycle collection handles this by making the “destroy A” step, that is the point where you make a group of objects unreachable, instead essentially mark the objects as potentially unreachable. The scan step then finds all objects that are unreachable, because you know none of the objects in some graph have external pointers, and thus nothing can acquire a new pointer to any of them, and not unreachable, because your scan didn’t find all the pointers, and thus something else by definition has at least one pointer and reference count.
The nice thing about this is that, in the best-case of all your objects just being created with a reference count of 1, and then being used a bit and then decremented from 1->0, objects are just the same as Arc
and freed immediately, with no extra work being needed. Even if the Arc’s count was incremented and then decremented twice instead (1->2->1->0) you can still immediately free the object: you know you had the last reference to the object, and thus it can’t possibly be leaked via cycle, and so you can just remove the object from the list and then free it immediately. The recursive scan is only ever needed to free objects that were actually leaking memory via cycles.
We visit objects in order to count how many pointers to them we’re able to find, because if we can find an equal number of pointers to an object as it’s reference count then we know the mutator doesn’t have any pointers, and if we find a disconnected group of those objects then we can free all of them at once since they’re collectively unreachable.
There’s a bunch of complex stuff that you have to handle in order to have correct visiting in the face of concurrrency, unfortunately - if you’re scanning a graph of objects, by the time you scan the last object that first object’s count may have changed. We use a scheme where objects have a bitmap of flags, and are marked VISITED when we first see it, and then if the object is decremented while VISITED it is transitioned to DIRTY. When checking to see if an object is leaked or not we can then also see if it is DIRTY or not, where we know that a mutator had a live reference to the object, and thus it and all reachable objects can be marked not leaked. The object is queued to be added to the next collection’s list instead (since the decrement may have been for the last external reference).
Additionally, consider what happens if we visit struct Foo { a: Mutex<Option<Gc<Bar>>>, b: Mutex<Option<Gc<Bar>>> }
concurrently with a mutator swapping the contents of foo.a
and foo.b
- we could count a
as one pointer to whatever it is referencing, and then the mutator swaps the field, and then we could count b
as a second pointer to…the exact same object as before, accidentally double-counting the first pointer’s object and under-counting the second’s, which would be very bad. The collector normally takes a read-lock on the Gc<T>
objects while it scans them, which either fails if object has an outstanding &mut
-reference from a mutator (in which case we obviously know it’s still reachable from something) or succeeds and blocks mutators from acquiring new &mut
-references which are required to swap around fields of objects. The entire point of interior mutability, however, is being able to modify fields with only a &
-reference! The simple solution, and what I did, is just require users to mark any type that has interior mutable fields containing Gc<T>
references, and take a write-lock instead of a read-lock if so, which guarantees the object is completely inaccessible even via &
-reference while we visit it.
Samsara is my library that provides a Gc<T>
type which is a reference counted with cycle collection smartpointer. It is a concurrent garbage collector, with the potentially-unreachable list scanning happening on another thread (only via manual trigger, not automatically via heuristiscs for now). And, unlike every(?) other Rust garbage collector that I know of, it doesn’t have a global stop-the-world phase: it will occasionally block mutator threads if they try to acquire a read-write reference from a Gc object while the collector is scanning it, but that should be very short (since it only holds the read-lock while visting that object, and not the entire subgraph). It doesn’t interrupt threads via signals or anything for safepoints, either, unlike some other garbage collectors.
Right now it’s actually implemented with only safe Rust, plus some unsafe libraries (im
for an immutable datastructure, and qcell
for an ergonomic get/set API to try and prevent user deadlocks). It is, literally, just Rust’s Arc<T>
behind the scenes, plus an RwLock<T>
for the interior mutability, with the collector list holding Weak<T>
references to objects that need to be scanned. Even if the collector incorrectly frees a set of objects that it thinks aren’t reachable any more, it would panic the user via an Option::unwrap
instead of giving a use-after-free.
It isn’t the most efficient currently. When doing a cycle collection it will probably use a lot of memory, especially if every live Gc object is reachable from one of the objects on the list, since it creates a big HashMap of objects and adjacency list of edges. It also currently has the problem where objects are sent to the list, or removed from the list, via sending a message on a channel to the collector thread, which means that if you trigger a collection and then decrement a bunch of Gc objects before it finishes, it will block on the channel since it isn’t being drained while a collection happens. I think that the “list” could instead be maintained via an instrusive doubly-linked-list in the Gc<T>
header, in order to allow the mutators to add or remove their objects from the list without needing the collector to do anything; unfortunately it would be hilariously unsafe and a lot of work to make sure it’s sound, and without a concrete usecase that is held back by the current design I probably won’t get around to trying that scheme out for a while.
I don’t claim to be doing anything novel here, for the record! For one, I’m obviously basing this off the paper from 2002, though doing things a little bit different. For another, there are several other Rust crates that also implement the paper! fitzgen’s bacon-rajan-cc and cactusref both are probably equally good if not better fits for anything you’d want to use samsara for - I mostly just nerdsniped myself and thought it would be neat to write this, and so I did.