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_add on update_pos. If it's greater than update_cap I return an error (size exhausted), otherwise I'm sure sure my coroutine/thread has exclusive access to updates[update_pos % update_cap], where I can put my update
Every X updates (if update_pos % 8 == 0 { //update tree} ) a coroutine will clone the tree, apply all pending updates, and then compare_and_swap the wftrie_ptr
When I want to read I do an atomic load on wtftrie_ptr and 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.
… I'm sure sure my coroutine/thread has exclusive access to updates[update_pos], where I can put my update
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 an UnsafeCell, which provides no synchronization and is unsafe to operate on:
updates: Vec<UnsafeCell<Update>>,
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”.
Every X updates … a coroutine will clone the tree …
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 the nodes and leaves vectors since that's the data you actually need.
If you were to use UnsafeCell or a lock as I mentioned above, then you'll find that you can't just clone the updates vector, 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.
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?
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.
If a coroutine is still holding a ref to the old tree during an update what happens? Should I replace the AtomicPtr with Arc?
Yes, this is a problem. Note that AtomicPtr is unsafe to read, because it does not guarantee the pointed-to thing is alive. You need a way to use something like Arc and atomically swap which specific Arc is in the wftrie_ptr location; the arc-swap library provides that.
Does this design fit well with the rust borrow checker or am I going to have to use unsafe?
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 that unsafe is 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”.
In the case of concurrent only scenario, can I drop atomics without using unsafe?
Yes; in a no-threads scenario, you can use Cell for all purposes that atomics would serve.
Related
I was wondering, is there a way to know the list of all smart pointers in Rust std?
I know String and Vec<T> are smart pointers, and reading Chp. 15 of the Rust book, I am learning about Box<T>, Rc<T>, Ref<T> and RefMut<T>
I was just wondering, is there a place to know all the available smart pointers in Rust's std?
I don't think an all-encompassing list would be particularly useful since there are lots (especially many which serve more as an implementation detail of another type). If you really want a complete list of everything that's technically a smart pointer, then as eggyal pointed out in a comment on your question you could browse the implementors of Deref, but that will probably give you more noise than useful information. I've listed some of the most common and useful ones below:
Box<T> - a unique pointer to an object on the heap. Analogous to C++'s std::unique_ptr.
Rc<T>/Weak<T> - a shared pointer that provides shared ownership of a value on a single thread. This smart pointer cannot be sent between threads safely since it does not use atomic operations to maintain its refcount (the compiler will make sure you don't accidentally do this).
Arc<T>/Weak<T> - very similar to Rc except it uses atomic operations to update its refcount, and thus is thread-safe. Similar to std::shared_ptr.
Vec<T>/String/PathBuf/OsString et al. - all of these are smart pointers for owning dynamically allocated arrays of items on the heap. Read their documentation for more specific details.
Cow<'a, B> - a clone-on-write smart pointer. Useful for when you have a value that could be borrowed or owned.
The list above isn't the full picture but it will get you very far with most of the code you write.
As you've noted there are other smart pointers like Ref and MutexGuard. These are returned by types with interior mutability, and usually have some kind of specific behavior on drop, such as releasing a lock or decrementing a refcount. Usually you don't interact with these types as much, but you can read their documentation on an as-needed basis.
There is also Pin<T>, but this smart pointer is notoriously hard to understand and really only comes up in conversations about the implementation details of futures and generators. You can read more about it here.
Given a rust object, is it possible to wrap it so that multiple references and a mutable reference are allowed but do not cause problems?
For example, a Vec that has multiple references and a single mutable reference.
Yes, but...
The type you're looking for is RefCell, but read on before jumping the gun!
Rust is a single-ownership language. It always will be. It's exactly that feature that makes Rust as thread-safe and memory-safe as it is. You cannot fully circumvent this, short of wrapping your entire program in unsafe and using raw pointers exclusively, and if you're going to do that, just write C since you're no longer getting any benefits out of using Rust.
So, at any given moment in your program, there must either be one thing writing to this memory or several things reading. That's the fundamental law of single-ownership. Keep that in mind; you cannot get around that. What I'm about to say still follows that rule.
Usually, we enforce this with our type signatures. If I take a &T, then I'm just an alias and won't write to it. If I take a &mut T, then nobody else can see what I'm doing till I forfeit that reference. That's usually good enough, and if we can, we want to do it that way, since we get guarantees at compile-time.
But it doesn't always work that way. Sometimes we can't prove that what we're doing is okay. Sometimes I've got two functions holding an, ostensibly, mutable reference, but I know, due to some other guarantees Rust doesn't know about, that only one will be writing to it at a time. Enter RefCell. RefCell<T> contains a single T and pretends to be immutable but lets you borrow the thing inside either mutably or immutably with try_borrow_mut and try_borrow. When we call one of these functions, we get a reference-like value that can read (and write, in the mutable case) to the original data, even though we started with a &RefCell<T> that doesn't look mutable.
But the fundamental law still holds. Note that those try_* functions return a Result, i.e. they might fail. If two functions simultaneously try to get try_borrow_mut references, the second one will fail, and it's your job to deal with that eventuality (even if "deal with that" means panic! in your particular use case). All we've done is move the single-ownership rules from compile-time to runtime. We haven't gotten rid of them; we've just changed who's responsible for enforcing them.
Rust has a feature to drain an entire sequence,
If you do need to drain the entire sequence, use the full range, .., as the argument. - Programming Rust
Why would you ever need to drain the entire sequence? I can see this documented, but I don't see any use cases for this,
let mut drain = vec.drain(..);
If draining does not take ownership but clears the original structure, what's the point of not taking ownership? I thought the point of a mutable reference was because the "book was borrowed" and that you could give it back. If the original structure is cleared why not "own" the book? Why would you want to only borrow something and destroy it? It makes sense to want to borrow a subset of a vector, and clearing that subset -- but I can't seem to wrap my head around wanting to borrow the entire thing clearing the original structure.
I think you are approaching this question from the wrong direction.
Having made a decision that you would like to have a drain method that takes a RangeBounds, you then need to consider the pros and cons of disallowing an unbounded RangeBounds.
Pros
If you disallowed an unbounded range, there would be less confusion about whether to use drain(..) vs into_iter(), although noting that these two are not exactly identical.
Cons
You would actually have to go out of your way to disallow an unbounded range.
Ideally, you would want the use of an unbounded range to cause a compilation error. I'm new to Rust so I am not certain of this, but as far as I know there is no way to express that drain should take a generic that implements trait RangeBounds as long as it is not a RangeFull.
If it could not be checked at compile time, what behavior would you want at runtime? A panic would seem to be the only option.
As observed in the comments and in the proposed duplicate, after completely draining it, the Vec will have a length of 0 but a capacity the same as it did before calling drain. By allowing an unbounded range with drain you are making it easier to avoid a repeated memory allocation in some use cases.
To me at least, the cons outweigh the pros.
I have a struct that contains a field that is rather expensive to initialize, so I want to be able to do so lazily. However, this may be necessary in a method that takes &self. The field also needs to be able to modified once it is initialized, but this will only occur in methods that take &mut self.
What is the correct (as in idiomatic, as well as in thread-safe) way to do this in Rust? It seems to me that it would be trivial with either of the two constraints:
If it only needed to be lazily initialized, and not mutated, I could simply use lazy-init's Lazy<T> type.
If it only needed to be mutable and not lazy, then I could just use a normal field (obviously).
However, I'm not quite sure what to do with both in place. RwLock seems relevant, but it appears that there is considerable trickiness to thread-safe lazy initialization given what I've seen of lazy-init's source, so I am hesitant to roll my own solution based on it.
The simplest solution is RwLock<Option<T>>.
However, I'm not quite sure what to do with both in place. RwLock seems relevant, but it appears that there is considerable trickiness to thread-safe lazy initialization given what I've seen of lazy-init's source, so I am hesitant to roll my own solution based on it.
lazy-init uses tricky code because it guarantees lock-free access after creation. Lock-free is always a bit trickier.
Note that in Rust it's easy to tell whether something is tricky or not: tricky means using an unsafe block. Since you can use RwLock<Option<T>> without any unsafe block there is nothing for you to worry about.
A variant to RwLock<Option<T>> may be necessary if you want to capture a closure for initialization once, rather than have to pass it at each potential initialization call-site.
In this case, you'll need something like RwLock<SimpleLazy<T>> where:
enum SimpleLazy<T> {
Initialized(T),
Uninitialized(Box<FnOnce() -> T>),
}
You don't have to worry about making SimpleLazy<T> Sync as RwLock will take care of that for you.
I am writing a service that will collect a great number of values and build large structures around them. For some of these, lookup tables are needed, and due to memory constraints I do not want to copy the key or value passed to the HashMap. However, using references gets me into trouble with the borrow checker (see example below). What is the preferred way of working with run-time created instances?
use std::collections::HashMap;
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
struct LargeKey;
struct LargeValue;
fn main() {
let mut lots_of_lookups: HashMap<&LargeKey, &LargeValue> = HashMap::new();
let run_time_created_key = LargeKey;
let run_time_created_value = LargeValue;
lots_of_lookups.insert(&run_time_created_key, &run_time_created_value);
lots_of_lookups.clear();
}
I was expecting clear() to release the borrows, but even if it actually does so, perhaps the compiler cannot figure that out?
Also, I was expecting clear() to release the borrows, but even if it actually does so, perhaps the compiler cannot figure that out?
At the moment, borrowing is purely scope based. Only a method which consumes the borrower can revoke the borrow, which is not always ideal.
What is the preferred way of working with shared run-time created instances?
The simplest way to expressed shared ownership is to use shared ownership. It does come with some syntactic overhead, however it would greatly simplify reasoning.
In Rust, there are two simple standard ways of expressing shared ownership:
Rc<RefCell<T>>, for sharing within a thread,
Arc<Mutex<T>>, for sharing across threads.
There are some variations (using Cell instead of RefCell or RWLock instead of Mutex), however those are the basics.
Beyond syntactic overhead, there's also some amount of run-time overhead going into (a) increasing/decreasing the reference count whenever you make a clone and (b) checking/marking/clearing the usage flag when accessing the wrapped instance of T.
There is one non-negligible downside to this approach, though. The borrowing rules are now checked at runtime instead of compile time, and therefore violations lead to panic instead of compile time errors.