我正在开发一个使用 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());
所以我有两个问题:
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
版本.
我真的很喜欢上面@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) {}
}