探索 Rust 异步简化编程

译者 | 弯月
译者 | 弯月 责编 | 欧阳姝黎
出品 | CSDN(ID:CSDNnews)
Rust的异步功能很强大,但也以晦涩难懂著称 。在本文中,我将总结之前提过的一些想法,并给出一些新的点子,看看这些想法放在一起能产生什么效果 。
本文只是一个思想实验 。对Rust进行大改造很麻烦,因此我们需要一个精确的方法来找出优缺点,并确定某个改动是否值得 。我知道一些观点会产生完全相反的看法,所以我建议你用一种开放的心态阅读本文 。
在对Rust中实现异步的不同方式进行探索之前,我们应该首先了解何时应该使用异步编程 。毕竟,异步编程并不像仅仅使用线程那么容易 。那么异步的好处是什么?有人会说是性能原因,异步代码更快,因为线程的开销太大了 。实际情况更复杂 。根据具体情况不同,在以I/O为主的应用程序中使用线程有可能更快 。例如,一个基于线程的echo服务器在并发数小于100的时候比异步更快 。但在并发数超过100之后,线程的性能就会下降,但也不是急剧下降 。
我认为,使用异步的更好的理由是可以更有效地针对复杂的流程控制进行建模 。例如,如果不适用异步编程,那么暂停或取消一个正在进行的操作就会非常困难 。而且,使用线程时,在各个连接之间进行协调需要使用同步原语,这就会导致竞争 。使用异步编程,可以在同一个线程中对多个连接进行操作,从而避免了同步原语 。
Rust的异步模型能够非常好地对复杂流程控制进行建模 。例如,mini-redis的subscribe命令(
https://github.com/tokio-rs/mini-redis/blob/master/src/cmd/subscribe.rs#L94-L156)就非常精练、非常优雅 。但异步也不是万能灵药 。许多人都认为异步Rust的学习曲线非常复杂 。尽管入门很容易,但很快就会遇到陡峭的曲线 。很多人付出了很多努力,尽管有几个方面有待改进,但我相信,异步Rust最大的问题就在于会违反“最小惊讶原则” 。
举个例子 。同学A在学习Rust时阅读了Rust的教科书和Tokio的指南,打算写一个聊天服务器作为练习 。他选了一个基于行的简单协议,将每一行编码,添加前缀表示行的长度 。解析行的函数如下:
let len = socket.read_u32.await?; let mut line = vec![0; len]; socket.read_exact(&mut line).await?; let line = str::from_utf8(line)?; Ok(line)}这段代码除了async和await关键字之外,跟阻塞的Rust代码没有什么两样 。尽管同学A从来没有写过Rust,但阅读并理解这个函数完全没问题,至少从他自己的角度看如此 。在本地测试时,聊天服务器似乎也能正常工作,于是他给同学B发送了一个链接 。但很不幸,在进行了一些聊天后,服务器崩溃了,并返回了“invalid UTF-8”的错误 。同学A很迷惑,他检查了代码,但并没有发现什么错误 。
那么问题在哪儿?似乎该任务在调用栈的更高层的位置使用了一个select!:
loop { select! { line_in = parse_line(&socket) => { if let Some(line_in) = line_in { broadcast_line(line_in); } else { // connection closed, exit loop break; } } line_out = channel.recv => { write_line(&socket, line_out).await; } }}假设channel上收到了一条消息,而此时parse_line在等待更多数据,那么select!就会放弃parse_line操作,从而导致丢失解析中的状态 。在后面的循环迭代中,parse_line再次被调用,从一帧的中间开始,从而导致读入了错误数据 。
问题在此:任何Rust异步函数都可能被调用者随时取消,而且与阻塞Rust不同,这里的取消是一个常见的异步操作 。更糟糕的是,没有任何新手教程提到了这一点 。

探索 Rust 异步简化编程

文章插图
 
Future如果能改变这一点,让异步Rust每一步的行为符合初学者预期呢?如果行为必须根据预期得到,那么必然有一个能接受的点,为初学者指引正确的方向 。此外,我们还希望最大程度地减少学习过程中的意料之外,特别是刚开始的时候 。
我们先来改变意料之外的取消问题,即让异步函数总是能够完成执行 。当future能够保证完成后,同学A发现异步Rust的行为跟阻塞Rust完全相同,只不过是多了两个关键字async和await而已 。生成新任务会增加并发,也会增加任务之间的协调通道数量 。select!不再能够接受任意异步语句,而只能与通道或类似通道的类型(例如JoinHandle)一起使用 。


推荐阅读