Allowing a read/write race


#1

I’m writing a simulator of a distributed system, with the simulation of each server running in its own thread. I’d like to query some elements in the server struct from the thread that spawns the servers. Since the server is mutable, that’s a read/write race. I don’t mind, since the query is only for debugging, and I know how to deal with any inconsistent state.

I tried wrapping the elements I want to look at in Arc::Mutex<_>, but that complicated the server code. Instead, I now have a wrapped copy for each variable that I query. (It’s not as bad as it sounds, since all relevant updates are in a single function.) Is there a better way to do this? I don’t mind using unsafe.


#2

What’s the type of data that you’re trying to read/write like that? How frequent are these racy reads? I assume not very frequent if it’s just for debugging.

Perhaps you can encapsulate the data in atomics, if their types align with that. Alternatively, set up a channel between the spawning thread and the server thread, and request debug info via messages over that channel.


#3

I prefer to leave the simulator code independent of the debugging interface, which is why I didn’t like wrapping the elements in Arc<Mutex<_>>. Atomics won’t work for that reason and the fact that the elements are mostly HashMap with complex structs as the values.

A channel is an interesting idea, but won’t I need a thread in the server to listen for debug requests?


#4

In that case, if you have a read/write race where the read occurs when the write side is in the middle of resizing a buffer, you end up with a real possibility of the HashMap impl crashing the process or even causing memory corruption. So it’s not for no reason that Rust prevents such races by default…


#5

I only look when the part of the system I’m interested in is quiescent, but I’m willing to live with a crash when I’m wrong. If there’s no way to do it even with unsafe code, I’ll just keep using shadow copies unless someone has a better idea.


#6

Yeah, but you’d need something similar to trigger a debug query as well even if it’s reading memory directly owned by one of the server threads? Or put another way, whatever trigger you’d use to inspect memory directly, you could use the same (or similar) trigger to send a debug request over the channel.

You really don’t want to go down the path of UB or crashes - build a proper debug/introspection interface :slight_smile:


#7

If UB reliably crashed, it wouldn’t be undefined :stuck_out_tongue: UB is far more insidious than just crashing.


#8

Reading a hashmap while it’s reallocating is going to be really crashy. If the Mutex has too high overhead, try RwLock or something from parking lot. Uncontended locks can be cheap.

Boilerplate can be mostly hidden with type aliases type AMHash<K,V> = Arc<Mutex<HashMap<K,V>>>. You can make your own trait to make shorter methods with .lock().unwrap() built-in.

This is a place where Rust just exposes the thing that’s necessary, where cutting of corners is really unsafe.


#9

Another way might be to use an immutable hash map. The struct itself is Send + Sync and cheap to clone, so your debugger could very cheaply clone a snapshot of the state, even from another thread.

Also when you need lots of fast hashmaps, try using a different hasher. Rust’s hashers can be plugged into stdlib’s hashmaps as well as the immutable one.


#10

Immutable hash map looks interesting, but I think I’ll just punt on the read/write race idea and stick with the wrapped copies I’m using now. Thanks for showing me all the ways I could have shot myself in the foot with my original idea.