asynchronousrustrust-tokio

Using dyn async traits (with async-trait crate) in spawned tokio task


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:

  1. Why doesn't my original example work? Is it some inconsistency between the two libraries I'm trying to use or am I approaching async programming in rust incorrectly?
  2. Is the solution of adding additional trait bounds the right way to solve this? I'm rather new to rust and have only been programming with it for a few months so I wouldn't be surprised to hear I'm just approaching this wrong.

Solution

  • 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.