I have worker threads communicating via shared states and channels for signalling. I want one worker to be able to write to an RwLock and another worker should only be able to read from it.
use std::{
sync::{mpsc, Arc, RwLock}, thread, time::Duration
};
pub struct State {
some_number: u8,
}
pub struct ImmutableWorker {}
impl ImmutableWorker {
pub fn new() -> Self { Self {} }
// I don't want to pass the RwLock as mutable here
pub fn run(&self, state: Arc<RwLock<State>>, event_receiver: mpsc::Receiver<()>) -> thread::JoinHandle<()> {
std::thread::spawn(move ||
loop {
thread::sleep(Duration::from_millis(1000));
if event_receiver.try_recv().is_ok() && state.read().unwrap().some_number > 0 {
println!("State number is above 0 so let's decrease it by one.");
state.write().unwrap().some_number -= 1;
println!("Contrary to popular belief, I was able to change the state some_number: {} So I'm not that immutable now eh?", state.read().unwrap().some_number);
} else {
println!("I can't receive any signals.");
}
}
)
}
}
pub struct MutatorWorker {
state: Arc<RwLock<State>>,
immutable_worker: ImmutableWorker,
}
impl MutatorWorker {
pub fn new() -> Self {
let state = Arc::new(RwLock::new(State { some_number: 0 }));
Self {
state,
immutable_worker: ImmutableWorker::new(),
}
}
pub fn run(&self) {
let (event_sender, event_receiver) = std::sync::mpsc::channel();
let _immut_handle = self.immutable_worker.run(Arc::clone(&self.state), event_receiver);
let state_clone = self.state.clone();
std::thread::spawn(move || {
loop {
thread::sleep(Duration::from_millis(500));
let added = state_clone.read().unwrap().some_number.saturating_add(1);
state_clone.write().unwrap().some_number = added;
println!("Here I increased the state some_number by one: {added} Noone else can change this number now (Hopefully).");
event_sender.send(()).unwrap();
}
});
}
}
fn main() {
let mut_worker = MutatorWorker::new();
mut_worker.run();
// I'm looping aimlessly to let other threads do their work.
loop {}
}
Running this code results in an output like this:
Here I increased the state some_number by one: 1 Noone else can change this number now (Hopefully).
State number is above 0 so let's decrease it by one.
Contrary to popular belief, I was able to change the state some_number: 0 So I'm not that immutable now eh?
Here I increased the state some_number by one: 1 Noone else can change this number now (Hopefully).
Here I increased the state some_number by one: 2 Noone else can change this number now (Hopefully).
State number is above 0 so let's decrease it by one.
Contrary to popular belief, I was able to change the state some_number: 1 So I'm not that immutable now eh?
Here I increased the state some_number by one: 2 Noone else can change this number now (Hopefully).
Here I increased the state some_number by one: 3 Noone else can change this number now (Hopefully).
State number is above 0 so let's decrease it by one.
Contrary to popular belief, I was able to change the state some_number: 2 So I'm not that immutable now eh?
Here I increased the state some_number by one: 3 Noone else can change this number now (Hopefully).
Here I increased the state some_number by one: 4 Noone else can change this number now (Hopefully).
State number is above 0 so let's decrease it by one.
Contrary to popular belief, I was able to change the state some_number: 3 So I'm not that immutable now eh?
Here I increased the state some_number by one: 4 Noone else can change this number now (Hopefully).
Here I increased the state some_number by one: 5 Noone else can change this number now (Hopefully).
What I want ideally is to pass an immutable reference to the RwLock's underlying data to the ImmutableWorker. Is there a way to achieve this?
Why don't you use a channel instead of an RwLock? One thread would be holding a Sender, the other - a Receiver. If you need to update some persistent state, you could use a separate thread to manage it. Thread A would be using its Sender to send updates to the state thread, which would perform the update and send updated info to thread B.
You could also write your own wrappers around RwLock, which would enforce read-only or write-only access. E.g.
In the original code for this example, state is a struct with around 3kb of data and it's shared between 4 different threads. State updates happen with 2 milliseconds intervals. I've been reading that channels, while being more precise in what they're doing, are inevitably slower. Docs about channels show that mpmc or multi producer, multi consumer channels are still experimental. Which discourages their use for carrying payloads to multiple consumers.
I tried this, but I couldn't mark ReadOnlyLock as thread safe: Rust Playground
This is a very good solution. It avoids wrapping around the RwLock<T> by limiting the type using trait bounds and exposing a thin API on top of RwLock. One problem I'm trying to solve with it though is can I limit the trait bound on the calling side ie. MutatorWorker sets the type as read-only but ImmutableWorker has no option but what is provided to it by the caller. In current solution, ImmutableWorker can modify its code to receive the RwLock as mutable.
2ms is more than enough time for channel overhead, which should be in the ones to tens of microseconds.
That experimental status only applies to including them in the standard library; the crossbeam crate has fully usable MP-MC channels available for several years now.
I don't understand what you're asking. The type determines whether mutation is allowed. If you use a read-only type in the interface to a function, then that function won't be able to mutate it at runtime.
If you change the type in the interface, then it is you deciding to allow mutation. This is the same as changing a parameter type from &Xxx to &mut Xxx. It is up to you to decide whether to make that change or not.
I believe OP is saying that with a trait they can modify the callee to get a RwLock they can mutate through, and that would compile without changes to the caller. They want something requiring a change on the caller side too.
I should explain it better. Let's say the calling function is some independent crate calling another crate's ImmutableWorker to run. Changes to the callee crate would affect how mutability is handled in the whole program. But a stricter contract like a ReadOnlyLock ensures this would be a breaking change rather than a silent, sneaky one.
The issue with that is that it wraps a reference to a RwLock, not the RwLock itself. This means it doesn't compose as well, e.g. Arc<ReadOnlyLock> is not the same as a read only Arc<RwLock>.
A more composable approach would be something like #[repr(transparent) struct ReadOnlyLock<T>(RwLock<T>);, which allows both &ReadOnlyLock and Arc<ReadOnlyLock> to work as expected, however creating them becomes a bit tricky and generally requires unsafe. It should be noted that some libraries can do this unsafe work for you, but they can also break the encapsulation by allowing anyone to cast it back to a RwLock.
Thank you all for participating in this. I think this contract (or a more generic, unspecialized solution similar to it) should be a part of the Rust standard library. I think there's a legitimate concern with principle of least priviledge here especially when crossing crate boundaries.