I need to share resources among multiple async tasks, I want to use Rc<RefCell<T>> when the a single thread scheduler is used, and Arc<Mutex<T>> for multi thread scheduler. but before I create my own trait, I want to know if there's a readily made library already, I don't want to re-invent the wheel. I can't find a suitable keyword to search. what do you guys use in such cases?
a little context: the resource will not be borrowed across awaits, and I only care about exclusive borrows (multiple borrows at the same time is a bug and a panic is desired in such situation). the task is runtime agnostic, and can happily run on either single thread or multi thread runtime. there are multiple such async tasks, I don't want to write the same logic twice for single thread and multi thread runtime. example of single threaded code roughly looks like:
async fn process_packets(sm: Rc<RefCell<StateMachine>>, socket: UdpSocket) {
let mut buffer = vec![0; 1024];
if let Ok((nbytes, peer)) = socket.recv_from(&mut buffer).await {
if sm.borrow_mut().accept(peer) {
while !sm.borrow_mut().done() {
let response = sm.borrow_mut().dispatch(&buffer[..nbytes]);
socket.send_to(&response[..], peer).await;
//... feed more packets to the state machine
}
} else {
// the spawn function can be made generic too,
// or I can pass a scheduler as argument
spawn_local(discover(sm.clone(), peer).detach();
}
}
}
async fn discover(sm: Rc<RefCell<StateMachine>>, peer: SocketAddr) {
//...
}
The concept is called interior mutability and there's no std trait abstracting over it. And that's probably for a good reason – whether such code is thread-safe tends to be far too important a distinction that can't usefully be abstracted away.
As it happens, the equivalent code with Arc<Mutex> would be incorrect, as you are performing multiple mutable borrows (which would correspond to multiple acquisitions of the lock in a threaded setting). In a multi-threaded environment, this would make your code racy, as another thread could intervene between two .lock()s and change the state of your state machine.
(This non-atomic pattern is equally dangerous in the single-threaded but async case, because it will suffer from the exact same race condition as soon as you accidentally start holding the borrow across await points. However, this is not the case in the specific piece of code you shared — yet…)
that's good reason I didn't think though. well I am just going to create a trait then. or maybe a macro.
thanks for the tips. the code snippet is just showing the structural aspect of the code. the actual state machine is implemented by an ffi library, and the input events to the state machine all contain additional info to distinguish them. although the state is not internally synchronized, the API calls are all atomic operations, different events can delivered interleaved, the states are not corrupted as long I don't multiple functions at the same time. so external locking on individual API call is enough, and race condition is not a concern in my case.
the reason I need borrow_mut() instead of borrow() is because the -sys binding demands it. what I was trying to do is to transform a convoluted polling based event loop into smaller (and hopefully composable) async tasks (and utilize the scheduler and reactor of the async ecosystem). the original code (without async) doesn't need RefCell at all, it's just a regular variable and a giant loop. it needs to be shared purely because I want to break it into smaller functions.
anyway, good to know it's not a common thing. I might end up doing it complete differently, I'm not satisfied with current solution after all.