无论是线程级还是进程级的并发,开销都很大.
Non-Blocking I/O
在 IO 密集型任务中,大部分线程处于阻塞状态,占用内存和 CPU 资源.所以一个 straightforward 方法就是少开点线程,用少量线程处理大量任务:当 socket 连接暂时没有数据的时候,线程不阻塞等待,而是去处理其他处于 ready 状态的任务.
一种解决办法是,我们反复调用系统轮询,直到 IO 就绪,例如 Linux 的 epoll、多路复用、kqueue 等等.但代价是比较浪费 CPU 资源.
协程
用户态线程(有时称为绿色线程),完全在用户态实现的类线程.
分为有栈协程,无栈协程.(是否发生上下文切换)
异步
异步就是上面所述的 idea.协程的核心想法和异步是差不多的,也是实现异步的一种方式.除了协程外,还包括回调函数、Javascript Promise、Reactive Programming 等等.
Rust 中的异步编程:无栈协程
future 代表了一个可能尚未计算完的值,是实现了 trait Future 的类型.Rust 中与异步相关的两个关键词
async把返回值变成futureawait等待某个future计算完成
我们接着来看 Future trait 的具体实现:
1 | pub trait Future { |
它有一个方法 poll() 作用就是尝试推进 Future,如果计算已完成,那么返回 Ready(T);否则没完成,返回 Pending.对于调用者来说,就是不断 poll() 尝试推进 future 直到返回 Ready(T).
对于
async, await关键字,实际上 Rust 是自动将其转化为了实现了Future的struct,所以其实是语法糖.
How Does Async Inform Completion
在异步 IO 时我们提到,当操作真正完成的时候,操作系统通过某种机制来通知应用程序.那么是什么机制呢?这个就和 poll() 方法的 context 参数有关了.
执行器在调用 poll() 的时候,会提供 Context,其中包含一个 waker.当 Future 需要等待的时候,它需要保存这个 waker 并在条件满足的时候调用 waker.wake()
我们以 Rust tokio 为例.tokio 包含两个核心组件:
Executor包含一个就绪队列,执行所有就绪任务,负责调度任务.Reactor负责监听 IO 时间,驱动 Waker 唤醒
总体流程类似于
- 注册 IO 事件。IO 方法检查是否有数据就绪:如果否,就在
Reactor中注册一个Interest并与当前任务的Waker关联起来,Reactor在 Linux 上通过epoll_ctl将需要 IO 操作的 fd 告诉 OS.最后返回Pending - 等待事件。
Reactor内部通过epoll_wait()阻塞或者轮询,当 fd 就绪时,OS 控制该方法返回. - 触发唤醒。
- 任务被再次调度