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}");
}
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.
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}");
}
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}");
}
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.
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! ↩︎
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
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.
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.
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).