jrpear
jrpear

Reputation: 262

How do the local variables in an async function affect the type of its future?

I encountered a strange error. Here's a MRE:

async fn do_nothing() { }

async fn use_dyn_read() {
    let read: &dyn std::io::Read = & (&[0 as u8; 1] as &[u8]);
    do_nothing().await;
}

#[tokio::main]
fn main() {
    tokio::spawn(use_dyn_read());
}

When I compile I get an error with this message:

error: future cannot be sent between threads safely
   --> src/main.rs:10:18
    |
10  |     tokio::spawn(use_dyn_read());
    |                  ^^^^^^^^^^^^^^ future returned by `use_dyn_read` is not `Send`
    |
    = help: the trait `Sync` is not implemented for `dyn std::io::Read`
note: future is not `Send` as this value is used across an await
   --> src/main.rs:5:17
    |
4   |     let read: &dyn std::io::Read = & (&[0 as u8; 1] as &[u8]);
    |         ---- has type `&dyn std::io::Read` which is not `Send`
5   |     do_nothing().await;
    |                 ^^^^^^ await occurs here, with `read` maybe used later
6   | }
    | - `read` is later dropped here
note: required by a bound in `tokio::spawn`
   --> /home/jrpear/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.21.2/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `tokio::spawn`

future is not 'Send' as this value is used across an await means that the type of local variables in an async function affect the type of its future.

However, some non-Send types don't cause this error. Using a Cell instead of a &dyn Read doesn't cause this error. Edit: I was wrong, Cell is Send (but not Sync)

What's going on here?

Upvotes: 1

Views: 995

Answers (2)

kmdreko
kmdreko

Reputation: 60447

Essentially, any local variable that is in-scope before and after an .await must be able to be persisted in the generated Future and will therefore affect its traits like Send and Sync. Here are some examples that demonstrate this:

struct IsSend;
struct NotSend(*const ()); // pointers are not Send

async fn is_send() {
    // This local variable is in scope across an await-point and is Send, so the
    // Future returned by this async function can also be Send.
    let _is = IsSend;
    futures::future::ready(()).await;
}

async fn not_send() {
    // This local variable is in scope across an await-point and is not Send, so
    // the Future returned by this async function can not be Send.
    let _not = NotSend(&());
    futures::future::ready(()).await;
}

async fn is_also_send() {
    {
        // This local variable is not Send but is not in scope across an await-
        // point, so Future returned by this async function can still be Send.
        let _not = NotSend(&());
    }
    futures::future::ready(()).await;
}

#[tokio::main]
async fn main() {
    tokio::spawn(is_send());
    // this does not compile since tokio::spawn requires Send
    // tokio::spawn(not_send()); 
    tokio::spawn(is_also_send());
}

The Cell type does implement Send (as long as its inner value is Send).

One thing that's tricky to remember is that references only implement Send if their referee implements Sync. In essence, a type being Sync means that it can be accessed from a different thread. So if it is Sync then its reference is Send since it can be sent to another thread and still refer to the original.

Trait objects are comparatively easier to understand, they only express the traits that they advertise. So a dyn Read does not implement Send or Sync since it doesn't say it does. So that coupled with our reference rules means that read needs to be &dyn Read + Sync in order to implement Send so it can be used across the .await while allowing the Future to implement Send.

Upvotes: 1

Kevin Reid
Kevin Reid

Reputation: 43842

dyn types implement only the traits that are explicitly written in them (and their supertraits). You don't get to automatically take advantage of trait implementations of the concrete type you're using, which is what happens in Rust code that doesn't use dyn. Instead, you have to pick the traits that will be exposed (and, equally, the traits that will be required of the concrete type).

You wrote dyn Read, so it doesn't implement Sync. Change it to dyn Read + Sync and the program compiles.

async fn use_dyn_read() {
    let read: &(dyn std::io::Read + Sync) = & (&[0 as u8; 1] as &[u8]);
    do_nothing().await;
}

(The reason we're writing Sync, not Send, is because we need the future to be Send, so we need the &dyn ... to be Send, and the rule for a immutable reference being Send is that the referent must be Sync — because an immutable reference grants shared access. However, in order to actually use this Read, you'll want to change the & to &mut, at which point you'll need + Send instead of + Sync.)

Upvotes: 1

Related Questions