How to use RefCell in multiple Futures?

Let's say I have a RefCell that needs to call borrow_mut() in a Future.

What would be some good strategies to work around this so that there's no runtime panic?

In case it matters, my actual use case is wasm futures and that requires futures to be 'static

The playground doesn't support 0.3 yet, but here's an example that can cargo run via nightly. I think using LocalSpawn/LocalPool is close enough to being like the JS requirements, but not 100% sure. Anyway I hit the same bug in this scenario :wink:

Note: removing the sleep will (maybe?) stop it from panicking, but the point of the sleep is to emulate the future doing some real work asynchronously

// lib.rs

use std::future::Future;
use std::rc::Rc;
use std::cell::RefCell;
use futures::executor::{LocalPool};
use futures::task::{LocalSpawn};

struct Foo {
    count: Rc<RefCell<u32>> 
}

impl Foo {
    pub fn load(&mut self) -> impl Future<Output=()> {
        let count = Rc::clone(&self.count);
        async move {
            let mut count = count.borrow_mut();
            println!("count is {}", count);

            //sleep in an async way so that we hold onto the borrow_mut()
            async_std::task::sleep(std::time::Duration::from_millis(1000)).await;

            *count += 1;
        }
    }
}

fn main() {
    let count = Rc::new(RefCell::new(0));
    let mut foo = Foo{count};

    let mut local_pool = LocalPool::new();
    let mut spawner = local_pool.spawner();

    //both of these futures will be run sortof concurrently - panics since count is `borrow_mut`'d twice
    spawner.spawn_local_obj(Box::new(foo.load()).into()).unwrap();
    spawner.spawn_local_obj(Box::new(foo.load()).into()).unwrap();
    
    local_pool.run();
}
// Cargo.toml dependencies
futures-preview = { version = "0.3.0-alpha.19", features = ["async-await"]}
async-std = "0.99.11"

output: thread 'main' panicked at 'already borrowed: BorrowMutError'

It looks like you need the semantics of a lock (eg. Mutex<T>) instead of a simple RefCell.

1 Like

Strictly by the how to use; you must drop the ref before any await.

More likely you switch type. RwLock matches RefCell behavior but Mutex is often fine.

1 Like

Thanks! Makes sense...

I wonder if there's another way by using a messaging channel to pass the data upon loading... might tinker around with that a bit too :slight_smile:

Using a Mutex is even more dangerous than a RefCell here, because instead of a panic you get a deadlock and it doesn't even provide any advantage over RefCell in a single threaded context. Just make sure you drop your borrow of the ref cell before any calls to await, and if you need it again after the await, borrow it again.

That said if all you're working with is an integer, just use atomics or a Cell.

2 Likes

Oh... so just to understand more completely - a deadlock happens whenever a Mutex tries to lock, but can't, because it's already locked by something in the same thread ?

Not quite sure why that is - an explanation would be great :slight_smile:

In my actual use case I can't really do that with the current design - one of the borrows happens in a game loop and the other is triggered by events (which may fire mid-loop).

However, I think I can work around it in another way....

Good point and glad you mentioned it... the usage of a u32 here was just to show a simple reproducible example, but good point!

Well the thing is, when you lock a mutex that is already locked, the code waits until the other lock is released. However if the original lock was done in the same thread, how will it ever get back and unlock that lock, if it's waiting for it to be unlocked?

Of course, deadlocks can also happen with several threads, but that typically requires at least two mutexes.

Just like you need to make sure to release the borrow of the ref cell before an await, you also need to release it before you call the function that goes and triggers those event handlers.

The basic idea is to keep the borrow as short as possible.

1 Like

I would think that it is unlocked when it goes out of scope- but since these futures are 'static ... I'm not really sure exactly where the scope ends :\

:100:

They are! The issue is that it hasn't gone out of scope yet. For example, I imagine that your event loop ends up looking something like this:

let shared = Rc::new(RefCell::new(0));
let handler = Handler { inner: shared.clone() };
loop {
    // Reset inner
    let mut borrow = shared.borrow_mut();
    *borrow = 0;
    
    handler.handle_events(); // will panic
}

playground

Notice how borrow has not gone out of scope when you call handle_events, which is why the linked playground above panics when run. If you drop the borrow before the call to handle_events, it wont panic:

let shared = Rc::new(RefCell::new(0));
let handler = Handler { inner: shared.clone() };
loop {
    // Reset inner
    let mut borrow = shared.borrow_mut();
    *borrow = 0;
    drop(borrow);
    
    handler.handle_events(); // will not panic
}

playground

I'm not talking about the scope or lifetime of the future. The important scope is the one of the return value from borrow or borrow_mut. That's the one that locks the RefCell.

Regarding await, the thing is that whenever you use await, your function may pause allowing the program to do something else while waiting, so calling await before the scope of the borrow ends is equivalent to my example above.

Note that if you have something like this:

shared.borrow_mut().some_async_fn().await

Then you will call await before the borrow is dropped. Instead try something like this

let future = shared.borrow_mut().some_async_fn();
future.await
3 Likes

gotta run but will check back on this later, thanks!!

Okay... right on, also learned another thing along the way (using drop explicitly instead of surrounding it with braces :D)

But please bear with me, I'm still struggling on this part:

How is shared definitively not still borrowed when future.await is called? Is it not possible for the future to hold a reference to the &mut self which was passed to some_async_fn ?

One thing that some people don't realize is that the lifetimes of things don't actually influence when destructor are run. Only the structure of the code determines this and the lifetimes are just used to verify that the structure is valid.

So if some_async_fn borrows from the borrow of shared and you wrote the call to await on a separate line such that it borrows from something that is dropped? Well then you just get a lifetime error at compile time. It's not going to keep the borrow alive for longer.

2 Likes

In fact, there's an alternative rust compiler named mrustc that doesn't even understand lifetimes, and assuming they are actually valid it can still compile Rust code correctly.

1 Like

You might want to consider using an async Mutex if you need to borrow across await points. That one will guarantee only one task can access and mutate the underlying data, and it will not block the executor in an async context.

There exists LocalMutex in futures-intrusive, which is an async Mutex for singlethreaded executors that is no-std compatible and actually has only insignificant more overhead compared to a RefCell.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.