背景: 同步 vs. 异步

通过对比同步编程来理解异步编程是最容易的方法, 所以我们先来看一个简单的同步示例:


# #![allow(unused_variables)]
#fn main() {
// reads 4096 bytes into `my_vec`
socket.read_exact(&mut my_vec[..4096]);
#}

这里的代码使用标准库的read_exact来从socket读取字节流. 让我们看看对应文档:

Read the exact number of bytes required to fill the given buffer. "读取恰好数量的字节, 并填充给定的缓存."

所以, 如果这个方法成功地返回, 我们能保证my_vec已经被填充, 也就是我们从socket 读取了4k字节. 太棒了!

但如果数据暂时还没有准备好呢? 如果数据还没从这个套接字(socket)那边传过来呢?

为了满足填充好缓存的保证, read_exact方法必须等待. 这也是术语"同步"的 来源: read_exact是和所需数据的可用性同步的.

更准确的说, read_exact阻塞了调用它的线程, 意味着该线程不能进一步执行, 直到 接收到需要的数据. 问题在于, 线程总体来说是一个太重量级的资源而不应该被浪费. 而且, 当一个线程被阻塞了, 它一直在做无用功; 所有的动作都发生在操作系统层面, 直到 数据可用, 并疏通了该线程.

放开来讲, 如果我们想要处理一堆连接, 而我们在用类似read_exact那样会阻塞线程的 方法, 那我们就需要给每个连接分配单独一个线程; 否则, 连接的处理会被阻塞以等待 另外的连接的活动完成. 就算我们能够协调连接的活动时间的分配, 线程的开销仍然会限制 系统的可伸缩性.

异步

为了达到更好的可伸缩性, 我们需要避免在等待资源释放的时候线程会被阻塞. 绝大部分的 操作系统提供一个"非阻塞"(或者叫异步)模式来和像套接字的对象进行交互. 在这个 模式里, 不能马上就绪(ready)的操作会返回一个错误, 然后允许你在当前线程继续做其他 工作.

人工地通过这种方式和资源打交道是相当痛苦的. 你可以指出如何在单线程中处理这些 "正在进行中"的操作, 然而大多数情况下, 这些操作来自于完全独立的不同活动(例如两个 分离的连接).

幸运的是, Rust提供了一种实现异步编程的方法, 这种方法感觉上像在使用多线程, 但 底层则是异步访问资源, 并且自动地为你糅合正在进行中的操作. 这方法的核心的概念就是 任务, 你可以把它当做是能够自动映射到更少数量的操作系统线程的"轻量级线程" (类似于goroutines)

让我们来了解任务模型是怎样工作的吧!