For some HPC computing I need to port a wait free trie (but you can think of it as a tree) from C to Rust. The trie is 99% read 1% write, and is used both in parallel-concurrent scenarios , and concurrent only scenarios (single thread with multiple coroutines). The size of the tree is usually between 100kb and 8 mb.
Basically the tree is like:
pub struct WFTrie {
nodes: Vec<Node>,
leaves: Vec<Leaf>,
updates: Vec<Update>,
update_pos: AtomicUsize,
update_cap: usize,
}
//....
let mut wftrie = WFTrie::new();
let wftrie_ptr = AtomicPtr::new(&mut wftrie);
//....
As you can see the trie already uses something very similar to the arena approach that many suggests, by using a vec and storing
When I want to update, I do a
fetch_and_addonupdate_pos. If it's greater thanupdate_capI return an error (size exhausted), otherwise I'm sure sure my coroutine/thread has exclusive access toupdates[update_pos % update_cap], where I can put my updateEvery X updates (
if update_pos % 8 == 0 { //update tree}) a coroutine will clone the tree, apply all pending updates, and thencompare_and_swapthewftrie_ptrWhen I want to read I do an atomic load on
wtftrie_ptrand I can access the tree, taking care of considering also the pending updates.
My questions are:
If I have multiple coroutines having an immutable reference, how can I have one coroutine doing updates? What's the most idiomatic way to translate this to rust?
If a coroutine is still holding a ref to the old tree during an update what happens? Should I replace the AtomicPtr with Arc?
Does this design fit well with the rust borrow checker or am I going to have to use unsafe?
In the case of concurrent only scenario, can I drop atomics without using unsafe?
I'm not particularly informed about HPC, but I hope I can give you some useful pointers about programming with concurrency in Rust.
This is necessary, but not sufficient: if you are going to write to data behind an
&reference (whether from multiple threads or not), then you are implementing interior mutability, and you must always signal that to the compiler by using some cell type. The minimal at-your-own-risk way to do that is with anUnsafeCell, which provides no synchronization and isunsafeto operate on:but you can also use a safer type such as a lock (that you know will never experience any contention since you arranged that no other thread is using it). In Rust, all locks and other interior mutability primitives — including atomics — are built on top of
UnsafeCell; it is how you tell the compiler “this memory is exempt from the usual rule of N readers xor 1 writer”.You will have to arrange so that the clone isn't trying to read the data that is being modified. That is, don't clone the
WFTrie, but only thenodesandleavesvectors since that's the data you actually need.If you were to use
UnsafeCellor a lock as I mentioned above, then you'll find that you can't just clone theupdatesvector, precisely because it wouldn't be sound to do so without explicit locking. If necessary, you can read it step-by-step in some way that agrees with your concurrency requirements.I'm not aware of a specifically Rust-y way to do this. It sounds like you could manage it with just an
AtomicBool, but you probably know more than I do here.Yes, this is a problem. Note that
AtomicPtrisunsafeto read, because it does not guarantee the pointed-to thing is alive. You need a way to use something likeArcand atomically swap which specificArcis in thewftrie_ptrlocation; thearc-swaplibrary provides that.From a perspective of using the data structure, it seems fine — it will not be any more inconvenient to read or write than any other data structure held behind
Arc.For implementation, you will not be having very much “fight with the borrow checker” because you are not going to be writing functions with very many borrows — since your data structure owns its contents. But, insofar as you are writing your own concurrency primitives with
unsafe, you will need to be careful to obey all the memory rules anyway. Remember thatunsafeis not a license to disregard the rules; it's telling the compiler “I'm going to obey the rules but in a way you can't check”.Yes; in a no-threads scenario, you can use
Cellfor all purposes that atomics would serve.