How to pass a reference to a trait object from async function to another async function?

Hello. In my Tokio-based app, I have a scenario that looks like this:

pub trait MyTrait: Send  {
    fn do_something(&self);
}

async fn my_task(param: Box<dyn MyTrait>) {
    /* ... */
}

fn main() {
    let tokio_runtime = Runtime::new().unwrap();

    let trait_object: Box<dyn MyTrait> = new_instance();
    let handle = tokio_runtime.spawn(my_task(trait_object));

    /* ...... */

    let _ = tokio_runtime.block_on(handle);
}

This works fine.

...until I need to pass the trait object to some sub-function, like:

async fn my_async_fun(param: &dyn MyTrait) {
    param.do_something();
}

async fn my_task(param: Box<dyn MyTrait>) {
    my_async_fun(param.as_ref()).await;
    my_async_fun(param.as_ref()).await;
}

...at which point, all of a sudden, this line from "main()" starts getting an error:

let handle = tokio_runtime.spawn(my_task(trait_object));

the trait Sync is not implemented for dyn MyTrait, which is required by impl Future<Output = ()>: Send
note: captured value is not Send because & references cannot be sent unless their referent is Sync

Please note that MyTrait is not Sync, and I really don't understand why it would have to be here, as it is never going to be accessed by more than one thread/task at the same time, right? :thinking:

What can I do to get around this?

It appears that I can pass the object forth and back, but that looks really weird:

async fn my_async_fun(param: Box<dyn MyTrait>) -> Box<dyn MyTrait> {
    param.do_something();
    param
}

async fn my_task(param: Box<dyn MyTrait>) {
    let param_gen2 = my_async_fun(param).await;
    my_async_fun(param_gen2).await;
}

Is there no way how I can pass it as a reference/borrow ???

Thank you.

1 Like

T: Sync is for<'a> &'a T: Send. Here's my favorite exploration of those traits. Note that the restraint is not only about serial access. But even if it was, the trait system is at too abstract of a layer to prove there is none here.

Other options include using dyn MyTrait + Sync or adding a Sync supertrait bound.

3 Likes

Yes, I could add Sync to the trait definition. But I don't see why this restriction should be required, as the task exclusively owns the trait object that is passed into it. There is no way how the object is accessed concurrently from different tasks/threads in this program, right? :confused:

Again, this works just fine with just the Send:

pub trait MyTrait: Send  {
    fn do_something(&self);
}

async fn my_task(param: Box<dyn MyTrait>) {
    param.do_something();
}

fn main() {
    let trait_object: Box<dyn MyTrait> = new_instance();
    let handle = tokio_runtime.spawn(my_task(trait_object));
}

Not working, though:

async fn my_async_fun(param: &dyn MyTrait) {
    param.do_something();
}

async fn my_task(param: Box<dyn MyTrait>) {
    my_async_fun(param.as_ref()).await;
    my_async_fun(param.as_ref()).await;
}

Interestingly, as I just figured out, using &mut references like this does work:

async fn my_async_fun(param: &mut dyn MyTrait) {
    param.do_something();
}

async fn my_task(mut param: Box<dyn MyTrait>) {
    my_async_fun(param.as_mut()).await;
    my_async_fun(param.as_mut()).await;
}

:sweat_smile:

I can live with the &mut references, but don't really need them to be mutable...

1 Like

Heh, you found the other thing I thought of before I could reply again (&mut _). The post I linked explains why that works.

There's nothing in the function API (trait bounds) preventing my_async_fun from sending the shared reference to another thread and concurrently accessing it except the lack of Sync. The compiler doesn't analyze function bodies to make sure everything is okay,[1] it enforces APIs. Needing a Sync bound for this case is like needing a Clone bound to clone.


  1. too expensive, too fragile, too specific of an interpretation of what's ok ↩︎

See also:

(Unstable.)

Well, okay. I would perfectly understand getting an error if my_async_fun actually tried to send the shared reference to another thread and concurrently access it – because that is clearly not allowed with a reference to something that is not Sync. But it does nothing of that sort!

Why does the compiler "proactively" forbid passing a reference to a non-Sync object into my_async_fun, just because the function might (in theory) be doing something that requires Sync, when actually (and easily to see) the function is not doing such a thing?

Why not check what the function actually does, and error out iff it actually attempts something forbidden? And I also don't understand by what logic a &mut reference fixes the problem. After all, the referenced object still is not Sync – but now it even has become mutable :fearful:

Rephrased: why restrain what function bodies can do based on the declared API?

Because then the declared API would mean much less to consumers; they'd have to read all function bodies too.

Because then changing the function body could break code that calls it.

Because e.g. the compiler doesn't know why a MutexGuard isn't Send, so (soundly) checking everything isn't possible.

Because of &mut _'s exclusivity.[1] Sync is about shared access. If you have a &mut, nothing else has access (absent something else causing UB); there is no sharing.


  1. &mut T is Send if T is Send. ↩︎

2 Likes

I haven't read through the linked material, but I wonder if it's the introduction of holding the trait object across awaits that force the resulting future to not be Send?

The tokio task spawn function requires the future to be send because the runtime may run it on a different thread.

I also think this is the issue.

By holding the reference across an await it will be stored in the future struct, and since the trait object is not Sync, the struct won't be Send and tokio's spawn will complain about it.

This comes up relatively often in program analysis and is because most of the time you really only have two choices:

  • allow only what can be proven to be ok (what Rust does) and accept that some valid programs won't be allowed;
  • disallow only what can be proven to not be ok (what you're asking) and accept that some invalid programs will be allowed (what is considered "unsound" in Rust and we want to avoid).

The call to tokio::spawn is the one that introduces the Send requirement (which results in a requirement for the pointed type by the reference to be Sync).

There have been some proposals to sidestep this issue, but AFAIK they are all incompatible with thread_local, which is already in the language and stable.

As mentioned before an iff is impossible in program analysis for most things.

Checking what a function actually does is also generally not very practical. It makes incremental compilation worse (which is already a pain point) and doesn't work with dynamic calls (i.e. calling function pointers or methods on trait objects).

1 Like

But the tokio::spawn here takes the trait object as an "owned" object (not reference!), so that the objects gets moved into the newly spawned task. It's clear that this requires the trait to be Send, and it is! But it should not, and it actually does not, require the trait to be Sync.

So there's no problem at all – until the task internally tries to pass a reference to it's owned object to another (async) function. At which point the initial tokio::spawn suddenly starts to error out. Why does the initial tokio::spawn fail, not the “problematic” sub-function call which happens inside of the task and which appears to be the actual source of the issue?

Also: When the task calls the other function, it does not spawn a separate task, but simply calls that function in the context of the current task, and it immediately awaits for the result in a totally "serial" fashion. So, at this point, nothing gets moved (or sent) to another thread/task. Then why should Sync suddenly be required when it wasn't before? And why does passing an immutable reference require Sync, but passing a &mut reference "removes" that requirement?

Figuiring out that &mut fixes the issue was like a "happy accident" :sweat_smile:

I use the word task here to differentiate between the future you pass to spawn directly, i.e. your call to my_task, and the futures you .await inside your task, i.e. your calls to my_async_fun. Calling my_async_fun creates a future that captures &dyn MyTrait in its state. Because &dyn Trait: !Send, the future won't be Send either. Every state that is held across an .await point (the future your are awaiting included) must be Send for your future to be Send as well. Because your task tries to .await a !Send future, it becomes !Send itself, violating the bounds on spawn, which is the error you are getting.

When you hit an .await point your task may get moved to a different thread, which is why it must be Send. For &T to be Send, T must be Sync.

&mut T is Send if T is Send, which is a requirement that your MyTrait fulfils. This SO answer from Alice explains this well IMO:

If a value is Send + !Sync, then it may be accessed in any way from any thread, but only from one thread at the time, even if the access is immutable.

&mut T - mutable references can't be copied, so sending them to other threads doesn't allow access from several threads in parallel, thus &mut T can be Send even if T is not Sync. Of course, T must still be Send.

2 Likes

You're missing the point. The analysis sees:

  • my_async_fun's Future contains a &dyn MyTrait so it is Send iff dyn Trait: Sync, which is not;
  • my_task's Future contains my_async_fun's Future, so it is Send iff that is also Send, which is not;
  • spawn requires my_task's Future to be Send, which is not, hence the error.

The fundamental issue is that my_async_fun is storing a &dyn MyTrait, which is not Send because dyn MyTrait is not Sync.

Because it is not an error for an async function to return a !Send Future. It can still be used with any executor that doesn't require a Send Future, unlike tokio::spawn.

1 Like

Still doesn't explain why storing a &mut dyn MyTrait (note the "mut"), which according to this logic is not Send either, I suppose, seems to be working fine.

As @jofas explained, &mut dyn MyTrait is Send when dyn MyTrait is Send (like in your case). It does not require Sync because there's no sharing.

1 Like

So, is using a &mut reference, even though I don't actually want/need mutability, the “proper” way to go in this kind of situation? Alternatives include passing an owned object to the sub-function and making the sub-function return it (seems a bit messy), or wrapping the object in a sync::Mutex and passing an immutable reference to the mutex. Going with a mutex seems like a pointless overhead, if we know for sure that the access will be 100% exclusive/serial.

Other alternatives would be making your trait Sync/using dyn MyTrait + Sync, changing the code so that a reference to them is not held during a .await or using a different runtime that doesn't need Send. All of these may not be viable though.

1 Like