Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rust: can tokio be understood as similar to Javascripts event loop or be used like it?

I'm not sure if tokio is similar to the event loop in Javascript, also a non-blocking runtime, or if it can be used to work in a similar way. In my understanding, tokio is an runtime for futures in Rust. Therefore it must implement some kind of userland threads or tasks, which can be achieved with an event loop (at least partly) to schedule new tasks.

Let's take the following Javascript code:

console.log('hello1');
setTimeout(() => console.log('hello2'), 0);
console.log('hello3');
setTimeout(() => console.log('hello4'), 0);
console.log('hello5');

The output will be

hello1
hello3
hello5
hello2
hello4

How can I do this in tokio? Is tokio meant to work like this overall? I tried the following code

async fn set_timeout(f: impl Fn(), ms: u64) {
    tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
    f()
}

#[tokio::main]
async fn main() {
    println!("hello1");
    tokio::spawn(async {set_timeout(|| println!("hello2"), 0)}).await;
    println!("hello3");
    tokio::spawn(async {set_timeout(|| println!("hello4"), 0)}).await;
    println!("hello5");
}

The output is just

hello1
hello3
hello5

If I change the code to

    println!("hello1");
    tokio::spawn(async {set_timeout(|| println!("hello2"), 0)}.await).await;
    println!("hello3");
    tokio::spawn(async {set_timeout(|| println!("hello4"), 0)}.await).await;
    println!("hello5");

The output is

hello1
hello2
hello3
hello4
hello5

but then I don't get the point of the whole async/await/future feature, because then my "async" set_timeout-tasks are actually blocking the other println statements..

like image 333
phip1611 Avatar asked Mar 02 '23 14:03

phip1611


1 Answers

In short: yes, Tokio is meant to work much like the JavaScript event loop. However, there are three problems with your first snippet.

First, it returns from main() before waiting for things to play out. Unlike your JavaScript code, which presumably runs in the browser, and runs the timeouts even after the code you typed in the console has finished running, the Rust code is in a short-lived executable which terminates after main(). Whatever things were scheduled to happen later won't occur if the executable stops running because it returned from main().

The second issue is that the anonymous async block that calls the set_timeout() async function doesn't do anything with its return value. An important difference between async functions in Rust and JavaScript is that in Rust you can't just call an async function and be done with it. In JavaScript an async function returns a promise, and if you don't await that promise, the event loop will still execute the code of the async function in the background. In Rust, an async function returns a future, but it is not associated with any event loop, it is just prepared for someone to run it. You then need to either await it with .await (with the same meaning as in JavaScript) or explicitly pass it to tokio::spawn() to execute in the background (with the same meaning as calling but not awaiting the function in JavaScript). Your async block does neither, so the invocation of set_timeout() is a no-op.

Finally, the code immediately awaits the task created by spawn(), which defeats the purpose of calling spawn() in the first place - tokio::spawn(foo()).await is functionally equivalent to foo().await for any foo().

The first issue can be resolved by adding a tiny sleep at the end of main. (This is not the proper fix, but will serve to demonstrate what happens.) The second issue can be fixed by removing the async block and just passing the return value of set_timeout() to tokio::spawn(). The third issue is resolved by removing the unnecessary .await of the task.

#[tokio::main]
async fn main() {
    println!("hello1");
    tokio::spawn(set_timeout(|| println!("hello2"), 0));
    println!("hello3");
    tokio::spawn(set_timeout(|| println!("hello4"), 0));
    println!("hello5");
    tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
}

This code will print the "expected" 1, 3, 5, 4, 2 (although the order is not guaranteed in programs like this). Real code would not end with a sleep; instead, it would await the tasks it has created, as shown in Shivam's answer.

like image 136
user4815162342 Avatar answered Mar 29 '23 22:03

user4815162342