3

How do you create an async recursive function that takes a mutex? Rust claims that this code holds a mutex across an await point. However, the value is dropped before the .await.

#[async_recursion]
async fn f(mutex: &Arc<Mutex<u128>>) {
    let mut unwrapped = mutex.lock().unwrap();
    *unwrapped += 1;
    let value = *unwrapped;
    drop(unwrapped);
    tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
    if value < 100 {
        f(mutex);
    }
}

Error

future cannot be sent between threads safely
within `impl futures::Future<Output = ()>`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, u128>`
required for the cast to the object type `dyn futures::Future<Output = ()> + std::marker::Send`rustc
lib.rs(251, 65): future is not `Send` as this value is used across an await
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Test
  • 962
  • 9
  • 26

1 Answers1

3

In this case, you can restructure the code to make it so unwrapped can't be used across an await:

let value = {
    let mut unwrapped = mutex.lock().unwrap();
    *unwrapped += 1;
    *unwrapped
};
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
if value < 100 {
    f(mutex);
}

If you weren't able to do this, then you'd need to make it so you don't return a Future that implements Send. The async_recursion docs specify an option you can pass to the macro to disable the Send bound it adds:

#[async_recursion(?Send)]
async fn f(mutex: &Arc<Mutex<u128>>) {
    ...

(playground)

You wouldn't be able to send such a Future across threads though.

smitop
  • 4,770
  • 2
  • 20
  • 53
  • Isn't this dangerous? Send exists for a reason. – Test Jun 24 '22 at 23:28
  • @Test I added a way to fix this without `?Send`. The second way isn't dangerous though, the compiler stops you if you try to send a type that can't be sent between threads that doesn't implement `Send`. – smitop Jun 24 '22 at 23:38
  • Ok, thank you. Why doesn't `drop` accomplish the same thing? – Test Jun 24 '22 at 23:46
  • 2
    @Test My understanding is that the compiler can't infer that `drop(unwrapped)` actually drops `unwrapped`, instead of perhaps sending it to another thread that would later use it after the `await` point (`drop` is just a normal function). – smitop Jun 24 '22 at 23:54
  • 1
    @Smitop While `drop` is a normal function, it moves the value and the compiler knows it. Little playing with the code suggests it is the lifetime `async_recursion` uses, but I'm not sure. [Drop tracking in async fns is known to be too conservative](https://github.com/rust-lang/rust/issues/57478). – Chayim Friedman Jun 25 '22 at 23:51