Why the code doesn't run async?

fn main(){
    futures::executor::block_on(async_test());
}
async fn async_test(){
    let task1=my_print("Task1",800);
    let task2=my_print("Task2",800);
    let task3=my_print("Task3",800);
    futures::join!(task1,task2,task3);
}
async fn my_print(name : &str, times: u64){
    let start =chrono::Local::now();
    for i in 1..times{
        let _ =1+1;
    }
    let end =chrono::Local::now();
    println!("Name: {name} / Times:{times} start: {start} end: {end}");
}

Output:

Name: Task1 / Times:800 start: 2023-08-31 15:46:53.584052700 +08:00 end: 2023-08-31 15:46:53.584203800 +08:00
Name: Task2 / Times:800 start: 2023-08-31 15:46:53.584629800 +08:00 end: 2023-08-31 15:46:53.584650600 +08:00
Name: Task3 / Times:800 start: 2023-08-31 15:46:53.587909300 +08:00 end: 2023-08-31 15:46:53.587934100 +08:00

The output shows the task ran one by one, I want to run async? Could you help me what's wrong?
Thanks.

You are confusing concurrency with parallelism.

1 Like

If you use tokio with rt-multi-thread and spawn each of your futures, then join the JoinHandles with futures::join! you will see them execute concurrently.

In release mode the compiler will optimize your for loop out. Try using the sleep future in tokio instead.

There is no await in your loop, so it cannot switch between the tasks during the loop. Please see this article: Async: what is blocking?, which should explain what's going on.

6 Likes

Please put ``` on a separate line before and after your code. And let rustfmt run on the code if you can. It's just much easier to read for most people.

fn main() {
    futures::executor::block_on(async_test());
}

async fn async_test() {
    let task1 = my_print("Task1", 800);
    let task2 = my_print("Task2", 800);
    let task3 = my_print("Task3", 800);
    futures::join!(task1, task2, task3);
}

async fn my_print(name: &str, times: u64) {
    let start = chrono::Local::now();
    for i in 1..times {
        let _ = 1 + 1;
    }
    let end = chrono::Local::now();
    println!("Name: {name} / Times:{times} start: {start} end: {end}");
}

Ouput:

Name: Task1 / Times:800 start: 2023-08-31 15:46:53.584052700 +08:00 end: 2023-08-31 15:46:53.584203800 +08:00
Name: Task2 / Times:800 start: 2023-08-31 15:46:53.584629800 +08:00 end: 2023-08-31 15:46:53.584650600 +08:00
Name: Task3 / Times:800 start: 2023-08-31 15:46:53.587909300 +08:00 end: 2023-08-31 15:46:53.587934100 +08:00
4 Likes

Here's an example how you can make your code run concurrently without true parallelism.
The flavor = "current_thread" let's tokio run on a single thread.
The nonsensical `tokio::time::sleep(Duration::from_secs(0)).await' demonstrates how each task can yield execution to the other tasks. Normally this would be some action that actually takes time, like a get request to a website for which we only have to wait.

use core::time::Duration;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    async_test().await;
}

async fn async_test() {
    let task1 = my_print("Task1", 800);
    let task2 = my_print("Task2", 800);
    let task3 = my_print("Task3", 800);
    tokio::join!(task1, task2, task3);
}

async fn my_print(name: &str, times: u64) {
    let start = chrono::Local::now();
    for _ in 1..times {
        let _ = 1 + 1;
        tokio::time::sleep(Duration::from_secs(0)).await
    }
    let end = chrono::Local::now();
    println!("Name: {name} / Times:{times} start: {start} end: {end}");
}
Name: Task2 / Times:800 start: 2023-08-31 10:51:54.801828534 +00:00 end: 2023-08-31 10:51:55.662617923 +00:00
Name: Task3 / Times:800 start: 2023-08-31 10:51:54.801831875 +00:00 end: 2023-08-31 10:51:55.662664124 +00:00
Name: Task1 / Times:800 start: 2023-08-31 10:51:54.801739862 +00:00 end: 2023-08-31 10:51:55.663769007 +00:00
1 Like

I am new with Rust. I want to why cannot three tasks run simultaneously?

Did you read Alice's link? The example in the article is almost your example code, even.

With cooperative scheduling, if you never yield control (with .await), no other task gets a chance to run.[1] The tasks have to cooperate. You cooperate by yielding -- by using .await.


  1. Modulo the number of threads utilized in the runtime, as noted in the part of the article talking about tokio::spawn. async generall doesn't want to rely on that; if you do want to rely on something like that... keep reading on through the next section of the article! ↩︎

3 Likes

I dosen't use [Tokio] crate. I just depend on [Futures] crate. I modify the code, marked by strong bold, I got result with no change.

Cargo.Toml
[package]
name = "Test"
version = "0.1.0"
edition = "2021"

[dependencies]
futures="0.3"
chrono = "0.4"

main.rs

fn main() {
    futures::executor::block_on(async_test());
}
async fn async_test() {
    let task1 = my_print("Task1", 80000);
    let task2 = my_print("Task2", 80000);
    let task3 = my_print("Task3", 80000);
    futures::join!(task1, task2, task3);
}

async fn my_print(name: &str, times: u64) {
    let start = chrono::Local::now();
    for i in 1..times {
        async {
             let _ = 1 + 1;
        }.await
    }
    let end = chrono::Local::now();
    println!("Name: {name} / Times:{times} start: {start} end: {end}");
}

Output

Name: Task1 / Times:80000 start: 2023-09-01 09:36:37.056227400 +08:00 end: 2023-09-01 09:36:37.058055400 +08:00
Name: Task2 / Times:80000 start: 2023-09-01 09:36:37.058323700 +08:00 end: 2023-09-01 09:36:37.060813 +08:00
Name: Task3 / Times:80000 start: 2023-09-01 09:36:37.061390 +08:00 end: 2023-09-01 09:36:37.064394200 +08:00

Is the compiler allowed to optimize async { 1 }.await to 1? (As opposed to, say, tokio::task::yield_now().await.)

From the link

I will be using the Tokio runtime for the examples, but the points raised here apply to any asynchronous runtime.

I think the async code needs to respond with Pending to a poll in order for a task swap to happen. If the current task can keep going then there's no reason to switch tasks as that would just be needless overhead: given your code, the most efficient way to execute it is to do it sequentially, rather than switching tasks thousands of times for no benefit.

If you use tokio's yield_now instead, the code does what you expect. yield_now's implementation looks like this

pub async fn yield_now() {
    /// Yield implementation
    struct YieldNow {
        yielded: bool,
    }

    impl Future for YieldNow {
        type Output = ();

        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
            ready!(crate::trace::trace_leaf(cx));

            if self.yielded {
                return Poll::Ready(());
            }

            self.yielded = true;

            context::defer(cx.waker());

            Poll::Pending
        }
    }

    YieldNow { yielded: false }.await
}

It basically just returns Pending once, and then Ready for subsequent polls.

Ah nice, I've been looking for something like this for my example. Have you ever used this in real code?

I just want to use only future crate. I don't want to use tokio.

You should still read the link. It explains what the problem is, and the problem is not specific to Tokio.

2 Likes

They can, but they aren't required to. Asnyc functions aren't threads.

An executor-independent yield_now() is easy enough to write, and indeed someone has made it as a separate crate. I'm a little surprised that futures doesn't already include it. The tokio version is just using a tokio-specific way of signalling that the task should be rescheduled, and is otherwise doing the same thing.

Isn't it the same as future::ready?

Not quite, future::ready returns the value you give it the first time you poll it, but the linked yield routine has to be polled twice.

1 Like

ready() always returns Poll::Ready so it would never yield. yield_now() returns Poll::Pending once, and then returns Poll::Ready, so it yields the first time it is reached.

1 Like

No, it's great that it's in tokio, but it's one of those features with a powerful scent. I think if I needed it I would have to ask "can I rewrite this future in a way that doesn't need to yield?"

My real question here is: what are we talking about? The code in the original post between the timers can and should be optimized away to nothing. I'm curious whether the compiler is allowed to optimize certain kinds of .await invocations as well. All the "yield" implementations use an explicit Future implementation. I tried making some examples in the playground but it seems a little tricky to get the assembly for the future (as opposed to just the assembly that creates an instance of that future).