使用 tokio::spawn 和 Box<dyn Error> 时如何修复“无法在线程之间安全发送”?

gue*_*ter 3 thread-safety rust trait-objects rust-tokio

这个简单的程序会产生编译器错误:

#[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(())
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

我真的不明白这个错误是什么意思。当我删除函数的返回类型时bar,它可以工作,但这里的实际错误是什么?

Cha*_*man 5

Box<dyn Error>是一种不透明类型。它可以包含任何类型。

假设它包含一种类型,该类型无法安全地发送到与创建它的线程不同的线程。例如, a MutexGuard,当从获取互斥锁的同一线程中删除该互斥锁时,必须释放该互斥锁。或者Rc,非原子地减少 drop 上的引用计数,因此如果移动到另一个线程,可能会导致 drop 上的数据争用。那么我们不应该将它发送到另一个线程。我们说类型没有实现Send。因为Box<dyn Error>可能包含这样的类型,所以它本身并没有实现Send

然而,tokio::spawn()未来可能会在.await点与线之间移动。这是为了提高效率:tokio使用工作窃取调度程序,这意味着它将任务转移到不太繁忙的线程(以及 CPU 核心)。但是假设Box<dyn Error>将包含一个未实现的类型Send,例如MutexGuard,它将在线程 A 中的调用之前创建tokio::join!(),并在调用之后(可能在线程 B 中)删除!(因为tokio::join()有一个隐含的.await)。这意味着这是不安全的,因此未来foo()也不安全Send,并且您不能将其派生到任务中。

修复方法很简单:确保Box<dyn Error>Send. + Send这可以通过添加一个绑定来完成:

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