rustthread-safetyrust-tokiotrait-objects

How do I fix "cannot be sent between threads safely" when using tokio::spawn and Box<dyn Error>?


This simple program yields a compiler error:

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        foo().await;
    });
}

async fn foo() {
    let f1 = bar();
    let f2 = bar();

    tokio::join!(f1, f2);
}

async fn bar() -> Result<(), Box<dyn std::error::Error>> {
    println!("Hello world");
    Ok(())
}
error[E0277]: `(dyn std::error::Error + 'static)` cannot be sent between threads safely
   --> src/main.rs:5:18
    |
5   |       tokio::spawn(async {
    |  _____------------_^
    | |     |
    | |     required by a bound introduced by this call
6   | |         foo().await;
7   | |     });
    | |_____^ `(dyn std::error::Error + 'static)` cannot be sent between threads safely
    |
    = help: the trait `Send` is not implemented for `(dyn std::error::Error + 'static)`
    = note: required for `Unique<(dyn std::error::Error + 'static)>` to implement `Send`
    = note: required because it appears within the type `Box<dyn Error>`
    = note: required because it appears within the type `Result<(), Box<dyn Error>>`
    = note: required because it appears within the type `MaybeDone<impl Future<Output = Result<(), Box<dyn Error>>>>`
    = note: required because it appears within the type `(MaybeDone<impl Future<Output = Result<(), Box<dyn Error>>>>, MaybeDone<impl Future<Output = Result<(), Box<dyn Error>>>>)`
    = note: required because it captures the following types: `ResumeTy`, `impl Future<Output = Result<(), Box<dyn Error>>>`, `(MaybeDone<impl Future<Output = Result<(), Box<dyn Error>>>>, MaybeDone<impl Future<Output = Result<(), Box<dyn Error>>>>)`, `&mut (MaybeDone<impl Future<Output = Result<(), Box<dyn Error>>>>, MaybeDone<impl Future<Output = Result<(), Box<dyn Error>>>>)`, `u32`, `[closure@join.rs:95:17]`, `PollFn<[closure@join.rs:95:17]>`, `()`
note: required because it's used within this `async fn` body

I don't really understand what the error means. When I remove the return type of the bar function it works, but what is the actual error here?


Solution

  • Box<dyn Error> is an opaque type. It may contain any type.

    Suppose it contains a type that cannot be safely send to a different thread than the one it was created in. For example, a MutexGuard, that must release the mutex when dropped from the same thread as it acquired it. Or an Rc, that decrements the reference count on drop non-atomically, and thus can cause a data race on drop if moved to another thread. Then we should not send it to another thread. We say the type does not implement Send. Because Box<dyn Error> may contain such types, it itself does not implement Send.

    However, tokio::spawn() may move the future between threads between .await points. This is to improve efficiency: tokio uses a work-stealing scheduler, meaning it will move tasks to threads (and therefore CPU cores) that are less busy. But suppose the Box<dyn Error> would contain a type that does not implement Send, for example MutexGuard, it would be created before the tokio::join!() call, in thread A, and dropped after it, potentially in thread B! (because tokio::join() has an implicit .await). This mean this is not safe, therefore the future of foo() is not Send either, and you cannot spawn it into a task.

    The fix is simple: ensure the Box<dyn Error> is Send. This can be done by adding a + Send bound to it:

    async fn bar() -> Result<(), Box<dyn std::error::Error + Send>> {
        println!("Hello world");
        Ok(())
    }