在生成的 tokio 任务中使用 dyn 异步特征(带有 async-trait 箱)

问题描述 投票:0回答:2

我正在开发一个使用 tokio 的异步 Rust 应用程序。我还想将一些特征方法定义为

async
并选择 async-trait crate 而不是夜间构建中的功能,以便我可以将它们用作
dyn
对象。但是,我在尝试在由
tokio::spawn
生成的任务中使用这些对象时遇到了问题。这是一个最小的完整示例:

use std::time::Duration;

use async_trait::async_trait;

#[tokio::main]
async fn main() {
    // These two lines based on the examples for dyn traits in the async-trait create
    let value = MyStruct::new();
    let object = &value as &dyn MyTrait;

    tokio::spawn(async move {
        object.foo().await;
    });
}

#[async_trait]
trait MyTrait {
    async fn foo(&self);
}

struct MyStruct {}

impl MyStruct {
    fn new() -> MyStruct {
        MyStruct {}
    }
}

#[async_trait]
impl MyTrait for MyStruct {
    async fn foo(&self) {
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
}

当我编译它时,我得到以下输出:

error: future cannot be sent between threads safely
   --> src/main.rs:11:18
    |
11  |       tokio::spawn(async move {
    |  __________________^
12  | |         object.foo().await;
13  | |     });
    | |_____^ future created by async block is not `Send`
    |
    = help: the trait `Sync` is not implemented for `dyn MyTrait`
note: captured value is not `Send` because `&` references cannot be sent unless their referent is `Sync`
   --> src/main.rs:12:9
    |
12  |         object.foo().await;
    |         ^^^^^^ has type `&dyn MyTrait` which is not `Send`, because `dyn MyTrait` is not `Sync`
note: required by a bound in `tokio::spawn`
   --> /home/wilyle/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.25.0/src/task/spawn.rs:163:21
    |
163 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`

error: could not compile `async-test` due to previous error

(将

object
let object: Box<dyn MyTrait> = Box::new(MyStruct::new());
装箱以及将结构完全移动到
tokio::spawn
调用内时,结果相似)

通过乱搞和尝试一些事情,我发现我可以通过装箱

object
并添加额外的特征边界来解决问题。在我的示例中用以下内容替换
main
的前两行似乎效果很好:

let object: Box<dyn MyTrait + Send + Sync> = Box::new(MyStruct::new());

所以我有两个问题:

  1. 为什么我原来的示例不起作用?我尝试使用的两个库之间是否存在不一致,或者我是否错误地使用 Rust 进行异步编程?
  2. 添加额外特征边界的解决方案是解决这个问题的正确方法吗?我对 Rust 还很陌生,并且只用它编程了几个月,所以当我听到我只是在接近这个错误时,我不会感到惊讶。
asynchronous rust rust-tokio
2个回答
5
投票

如果您不确定

Send
Sync
的含义,请查看这些文档链接。需要注意的是,如果
T
Sync
,那么
&T
就是
Send

问题 #2 很简单:是的,这是正确的方法。出于基本相同的原因,

async-trait
使用
Pin<Box<dyn Future + Send>>
作为其返回类型。请注意,您只能将 auto Traits 添加到 Trait 对象。

对于问题 #1,有两个问题:

Send
'static

Send

当您将某些内容转换为

dyn MyTrait
时,您将删除所有原始类型信息并将其替换为类型
dyn MyTrait
。这意味着您失去了
Send
上自动实现的
Sync
MyStruct
特征。
tokio::spawn
功能需要
Send

这个问题不是异步所固有的,这是因为

tokio::spawn
将在其线程池上运行 future,可能会将其发送到另一个线程。你可以在没有
tokio::spawn
的情况下运行 future,例如这样:

fn main() {
    let runtime = tokio::runtime::Runtime::new().unwrap();
    
    let value = MyStruct::new();
    let object = &value as &dyn MyTrait;

    runtime.block_on(object.foo());
}

block_on
函数在当前线程上运行future,因此
Send
不是必需的。它会阻塞直到未来完成,所以也不需要
'static
。这对于在运行时创建并包含程序的整个逻辑的东西来说非常有用,但是对于
dyn Trait
类型,您通常会发生其他事情,这使得它没有那么有用。

'static

当某些东西需要

'static
时,这意味着所有引用都需要与
'static
一样长。满足这一要求的一种方法是删除所有引用。在理想的世界中,你可以这样做:

let object = value as dyn MyTrait;

但是,Rust 不支持堆栈上动态大小的类型或作为函数参数。我们正在尝试删除所有引用,因此

&dyn MyTrait
不起作用(除非您 leak 或有静态变量)。
Box
允许您通过将动态大小的类型放在堆上来拥有它们的所有权,从而消除生命周期。

为此您需要

Send
,因为从
Sync
Send
的升级仅适用于
&
,而不适用于
Box
。相反,当
Box<T>
Send
时,
T
Send

Sync
更微妙。虽然
spawn
不需要
Sync
,但异步块确实需要
Send + Sync
Send
。由于
foo
接受
&self
,这意味着它返回一个包含
Future
&self
。然后对该类型进行轮询,因此在轮询之间
&self
可以在线程之间发送。和以前一样,如果
&T
Send
,那么
T
就是
Sync
但是,如果你将其更改为
foo(&mut self)
,它将在没有
+ Sync
的情况下编译。
有意义,因为现在它可以检查它是否没有同时使用,但在我看来,未来可能会允许
&self
版本.


0
投票

我真的很喜欢上面@drewtato给出的答案,但这里有另一个独立的例子,它解决了帖子的标题,尽管不一定是确切的问题:

这里我们有一个

Callback
,我们想要将其移至新生成的任务中。我们将其作为
Arc
传递,因为我们将拥有该对象的多个所有者。如前所述,我们需要使用
dyn
,因为其大小在编译时未知,但这意味着我们会丢失
Sync
Send
...但我们可以将它们再次添加到特征边界中。

#[async_trait]
pub trait Callback {
    async fn call(&self);
}

pub async fn start_stuff(callback: Arc<dyn Callback + Send + Sync>) {
    let callback = callback.clone();
    tokio::spawn(async move { callback.call().await });
}

现在我们可以像 API 的用户一样使用它......

pub async fn call_stuff() {
    let callback = Arc::new(Handler {});
    start_stuff(callback);
}

struct Handler {}
#[async_trait]
impl Callback for Handler {
    async fn call(&self) {}
}
© www.soinside.com 2019 - 2024. All rights reserved.