Why can't a dropped MutexGuard be held across an await point

This code doesn't compile:

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn t() {
        tokio::spawn(async {
            let m = std::sync::Mutex::new("hi".to_string());
            let guard = m.lock().unwrap();
            let value = guard.clone();
            drop(guard);

            tokio::time::sleep(std::time::Duration::from_micros(1)).await;

            println!("{value}");
        });
    }

with this error message:

error: future cannot be sent between threads safely
    --> src/net/dns.rs:1132:9
     |
1132 | /         tokio::spawn(async {
1133 | |             let m = std::sync::Mutex::new("hi".to_string());
1134 | |             let guard = m.lock().unwrap();
1135 | |             let value = guard.clone();
...    |
1140 | |             println!("{value}");
1141 | |         });
     | |__________^ future created by async block is not `Send`
     |
     = help: within `{async block@src/net/dns.rs:1132:22: 1132:27}`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard<'_, std::string::String>`, which is required by `{async block@src/net/dns.rs:1132:22: 1132:27}: std::marker::Send`
note: future is not `Send` as this value is used across an await
    --> src/net/dns.rs:1138:69
     |
1134 |             let guard = m.lock().unwrap();
     |                 ----- has type `std::sync::MutexGuard<'_, std::string::String>` which is not `Send`
...
1138 |             tokio::time::sleep(std::time::Duration::from_micros(1)).await;
     |                                                                     ^^^^^ await occurs here, with `guard` maybe used later
note: required by a bound in `tokio::spawn`
    --> /home/jonathan/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.38.1/src/task/spawn.rs:166:21
     |
164  |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
     |            ----- required by a bound in this function
165  |     where
166  |         F: Future + Send + 'static,
     |                     ^^^^ required by this bound in `spawn`

This really surprises me, and there seems to be an incorrectness in my mental model of rust async. We are dropping guard, so why is it still alive at the .await?

I know that I can rewrite the code as

            let value = m.lock().unwrap().clone();

or

            let value = {
                let guard = m.lock().unwrap();
                guard.clone()
            };

and it will compile, but this is not my question. My question is: Why can't I hold the MutexGuard across the await point even though it's dropped? Is there some bigger misunderstanding in what I thought drop(guard) does, or is this just a peculiarity of how async state machines are built?

It's a shortcoming of the current compiler analysis. One of the issues listed in this PR.

4 Likes

I’d also suggest that one should make a habit of using {} blocks instead of drop() anyway. This is because, in general, there may be other variables that one is not thinking as hard about, and a block means all the variables in the block are dropped, not just the ones you explicitly passed to drop. This can keep the memory usage of your application lower — not just in heap allocations but also in the size of the generated Future value itself.

It may even be sometimes worth moving code out of the async block/fn into a separate plain fn when possible. The compiler’s currently not that great at building the hidden coroutine state machines that async blocks/fns compile to, which can result in them being unnecessarily large or very slow to compile, and so you can help out by minimizing the amount of code and data that needs the full async treatment.

(But even if the compiler did its job perfectly, it's still required by the language semantics to drop variables at the end of scope and no sooner, so introducing blocks or drops is good for minimizing resource usage even in that hypothetical.)

4 Likes

Thanks a lot for both of your answers :heart:!