Software Design Problem for Rust

I came across a problem with software design for Rust.

Sometimes a module may have some child modules which has it's own async run method with loop task.

In the module behavior layer, the parent module has a role of child service orchestration, such as get some data from child A, and do some work to push to another child B.

In my code like this:

trait TraitA {
    async fn run(&mut self) -> Result<()>;
    // ....
}

trait TraitB {
    async fn run(&mut self) -> Result<()>;
    // ....
}

struct Father<A: TraitA, B: TraitB> {}

async fn run(&mut self) -> Result<()> {
    let mut a = self.a.take().unwrap();
    a_send = a.sender();
    a_recv = a.recver();
    tokio::spawn(async move { a.run().await; });

    let mut b = self.b.take().unwrap();
    b_send = b.sender();
    b_recv = b.recver();
    tokio::spawn(async move { b.run().await; });


    loop {
        // listen a_recv.
        match a_recv.try_recv() {
            /* do some works */
        }

        // send data to father's father.
        self.recv_tx.send(some_data).await?;
    }
}

But I got an error saying that child A and B don't satisfy Send trait even though I wrote trait A: Send { /* sth. */ }

I am also a Go developer, constructing an abstract system first is my habit to write code. I want to know the right way and habit to write code in Rust, Thanks so much!

What is the actual compiler error? "Child A and B" don't appear in your source code so it's hard to say what's wrong here.

Thanks for your answer.

The compiler tells me how to resolve it, but it's not elegant enough, what's the point of having the keyword async if it's all the same?

error: future cannot be sent between threads safely
   --> src/connect/mod.rs:151:9
    |
151 |         tokio::spawn(async move { core.run().await });
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ future created by async block is not `Send`
    |
    = help: within `{async block@src/connect/mod.rs:151:22: 151:32}`, the trait `Send` is not implemented for `impl Future<Output = ()>`, which is required by `{async block@src/connect/mod.rs:151:22: 151:32}: Send`
note: future is not `Send` as it awaits another future which is not `Send`
   --> src/connect/mod.rs:151:35
    |
151 |         tokio::spawn(async move { core.run().await });
    |                                   ^^^^^^^^^^ await occurs here on type `impl Future<Output = ()>`, which is not `Send`
note: required by a bound in `tokio::spawn`
   --> /Users/tomyang/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.40.0/src/task/spawn.rs:167:21
    |
165 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- required by a bound in this function
166 |     where
167 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`
help: `Send` can be made part of the associated future's guarantees for all implementations of `A::run`
    |
135 -     async fn run(&mut self);
135 +     fn run(&mut self) -> impl std::future::Future<Output = ()> + Send;
    |


Ah I see. You can read about why the Send bound is required here (Tokio tutorial)

The examples there seem to suggest you can ensure your async functions impl Send by ensuring you don't hold any data that isn't Send (such as Rc) over an await point within the function.

Unfortunately in your case, as the functions are trait methods, the compiler cannot know that all the types implementing the traits are ensuring this. So it has to be cautious in requiring that the async function itself is Send in the bound

and you end up with a desugared fn signature.

I.e. async fn is syntax sugar for a regular fn returning a Future. There doesn't exist any sugar to specify the Send bound (as far as I'm aware).

I agree it is inelegant. I think it would make a good user story for the async working group to consider.

As for what is the point of async syntax? Well, it helps in common cases.

Take what I've said with a pinch of salt. I'm far from an async expert. @alice might want to correct me :slight_smile:

There could also be a different way of structuring this thing that you're doing which avoids an ugly fn signature. One idea I have is, what if you simply to tokio::spawn(a.run) (might need to ensure a is &mut A and not an owned A) instead of spawning an async block that immediately awaits an async function? Might not make any difference, not sure!

They know. They made the trait-variant library so that a solution is available until better syntax is.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.