Tokio可以理解为类似于JavaScript的事件循环,或者像它一样使用吗?

7

我不确定 tokio 是否类似于JavaScript中的事件循环,也是一种非阻塞运行时,或者它是否可以用类似的方式工作。在我的理解中,tokio是一个用于Rust中的futures的运行时。因此,它必须实现某种用户级线程或任务,这可以通过事件循环(至少部分地)来调度新任务。

让我们看一下以下JavaScript代码:

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

输出结果将会是:

hello1
hello3
hello5
hello2
hello4

我该如何在tokio中实现这个?tokio整体上是这样工作的吗?我尝试了以下代码

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");
}

输出仅为

hello1
hello3
hello5

如果我更改代码为

    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");

输出为

hello1
hello2
hello3
hello4
hello5

但这样做我就不太明白整个异步/等待/未来功能的意义了,因为我的“异步”set_timeout任务实际上会阻塞其他println语句。


1
main 结束时,你必须等待所有的操作完成。Tokio 不会为你处理 - 当 main 退出时,事件循环会立即关闭。 - Cerberus
我在本地和游乐场中尝试了这段代码,得到了相同的结果。如何才能在Tokio中实现与Javascript事件循环相同的行为?Tokio是否本来就是这样工作的? - phip1611
2个回答

9
简而言之:是的,Tokio的工作原理类似于JavaScript事件循环。但是,你的代码有三个问题。
首先,在等待事情发生之前就从main()函数中返回了。与你在浏览器中运行的JavaScript代码不同,即使你在控制台输入的代码已经完成运行,JavaScript仍会运行超时函数。而Rust代码是一个短暂的可执行文件,一旦从main()返回,它就会终止。如果可执行文件停止运行,那么安排后续发生的任何事情都不会发生。
第二个问题是,匿名的异步块调用set_timeout()异步函数,却没有对其返回值进行任何操作。在Rust和JavaScript中,异步函数之间的一个重要区别是,不能只是简单地调用异步函数。在JavaScript中,异步函数返回一个Promise对象,如果你不等待该Promise对象,事件循环仍将在后台执行异步函数的代码。在Rust中,异步函数返回一个Future对象,但它不与任何事件循环相关联,而是为某些人准备好运行它。然后,你需要使用.await来等待它(与在JavaScript中的意思相同),或者显式地将它传递给tokio::spawn()在后台执行(与在JavaScript中调用但不等待函数的意思相同)。你的异步块都没有做到这两点,因此对set_timeout()的调用是无效的。
最后,代码立即等待spawn()创建的任务,这使得调用spawn()失去了意义-tokio::spawn(foo()).await在任何foo()情况下都等价于foo().await
第一个问题可以通过在main函数末尾添加一个小睡眠来解决。(这不是正确的修复方法,但将演示发生了什么。)第二个问题可以通过删除异步块并将set_timeout()的返回值直接传递给tokio::spawn()来解决。第三个问题可以通过删除任务的不必要的.await来解决。
#[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;
}

这段代码将会打印出 "expected" 的数字序列为 1、3、5、4、2 (尽管程序的顺序不能保证)。真实的代码不会以 sleep 结尾;相反,它会等待已创建的任务完成,就像 Shivam 的回答中所示。

谢谢你的回答。我想指出tokio::spawn(foo()).awaitfoo().await并不完全等价。考虑一个需要很长时间且没有yield的foo()。在foo().await版本中,直到foo()完成之前,此线程上的其他任务将不会执行。而在tokio::spawn(foo()).await中,执行被yielded,因此在等待foo()完成时将执行所有任务。 - Özgür Murat Sağdıçoğlu
@OzgurMurat 我认为一个不产生yield的 foo() 是有缺陷的,需要修复(例如,通过在 tokio::spawn_blocking() 中包装需要太长时间的部分)。Tokio支持单线程和多线程执行器。即使在多线程上下文中,依赖另一个线程执行不产生yield的代码也是危险的,如果多个协程尝试相同的技巧,并且每个执行器线程都被阻塞,系统将停滞不前。 - user4815162342

4
与JavaScript不同,Rust在异步函数的执行之前不会启动future。这意味着set_timeout(|| println!("hello2"), 0)只创建了一个新的future,而没有执行它。只有在等待它时,它才被执行。.await会阻塞当前线程,直到未来完成,这并不是"真正的异步应用程序"。为了使您的代码像JavaScript一样并发,您可以使用join!宏:-
use tokio::join;
use tokio::time::*;

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

#[tokio::main]
async fn main() {
    println!("hello1");
    let fut_1 = tokio::spawn(set_timeout(|| println!("hello2"), 0));
    println!("hello3");
    let fut_2 = tokio::spawn(set_timeout(|| println!("hello4"), 0));
    println!("hello5");

    join!(fut_1, fut_2);
}

如果想要体验 Promise.all 的感觉,可以使用 FuturesOrdered

更多信息请参见:


1
如果您想要一个更简单的API,可以使用join_all - piojo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接