I'm working on an asynchronous rust application which utilizes tokio. I'd also like to define some trait methods as async
and have opted for the async-trait crate rather than the feature in the nightly build so that I can use them as dyn
objects. However, I'm running into issues trying to use these objects in a task spawned with tokio::spawn
. Here's a minimal complete example:
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;
}
}
When I compile this I get the following output:
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
(The results are similar when making object
boxed with let object: Box<dyn MyTrait> = Box::new(MyStruct::new());
and when moving the construction fully inside the tokio::spawn
call)
By messing around and trying a few things I found that I could solve the issue by boxing object
and adding additional trait bounds. Replacing the first two lines of main
in my example with the following seems to work just fine:
let object: Box<dyn MyTrait + Send + Sync> = Box::new(MyStruct::new());
So I have two questions:
If you're not sure what Send
and Sync
mean, check out those documentation links. Something to note is that if T
is Sync
, then &T
is Send
.
Question #2 is simple: yes this is the right way to do it. async-trait
uses Pin<Box<dyn Future + Send>>
as its return type for basically the same reasons. Note that you can only add auto traits to trait objects.
For question #1, there's two issues: Send
and 'static
.
Send
When you cast something as dyn MyTrait
, you're removing all the original type information and replacing it with the type dyn MyTrait
. That means you lose the auto-implemented Send
and Sync
traits on MyStruct
. The tokio::spawn
function requires Send
.
This issue isn't inherent to async, it's because tokio::spawn
will run the future on its threadpool, possibly sending it to another thread. You can run the future without tokio::spawn
, for example like this:
fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
let value = MyStruct::new();
let object = &value as &dyn MyTrait;
runtime.block_on(object.foo());
}
The block_on
function runs the future on the current thread, so Send
is not necessary. And it blocks until the future is done, so 'static
is also not needed. This is great for things that are created at runtime and contain the entire logic of the program, but for dyn Trait
types you usually have other things going on that makes this not as useful.
'static
When something requires 'static
, it means that all references need to live as long as 'static
. One way of satisfying that is to remove all references. In an ideal world you could do:
let object = value as dyn MyTrait;
However, rust doesn't support dynamically sized types on the stack or as function arguments. We're trying to remove all references, so &dyn MyTrait
isn't going to work (unless you leak or have a static variable). Box
lets you have ownership over dynamically sized types by putting them on the heap, eliminating the lifetime.
You need Send
for this because the upgrade from Sync
to Send
only happens with &
, not Box
. Instead, Box<T>
is Send
when T
is Send
.
Sync
is more subtle. While spawn
doesn't require Sync
, the async block does require Send + Sync
to be Send
. Since foo
takes &self
, that means it returns a Future
that holds &self
. That type is then polled, so in between polls &self
could be sent in between threads. And as before, &T
is Send
if T
is Sync
. However, if you change it to foo(&mut self)
it compiles without + Sync
. Makes sense since now it can check that it's not being used concurrently, but it seems to me like the &self
verison could be allowed in the future.