操作系统可以看作是个虚拟机(VM),进程生活在操作系统创造的虚拟世界里。进程不用知道到底有多少 core 多少内存,只要进程不要索取的太过分,操作系统就假装有无限多的资源可用。基于这个思想,线程(Thread)的个数并不受硬件限制:你的程序可以只有一个线程、也可以有成百上千个。操作系统会默默做好调度,让诸多线程共享有限的 CPU 时间片。这个调度的过程对线程是完全透明的。那么,操作系统是怎样做到在线程无感知的情况下调度呢?答案是上下文切换(Context Switch),简单来说,操作系统利用软中断机制,把程序从任意位置打断,然后保存当前所有寄存器——包括最重要的指令寄存器 PC 和栈顶指针 SP,还有一些线程控制信息(TCB),整个过程会产生数个微秒的 overhead。然而作为一位合格的程序员,你一定也听说过,线程是昂贵的:
A function written in continuation-passing style takes an extra argument: an explicit “continuation”, i.e. a function of one argument. When the CPS function has computed its result value, it “returns” it by calling the continuation function with this value as the argument.CPS 风格的函数带一个额外的参数:一个显式的 Continuation,具体来说就是个仅有一个参数的函数。当 CPS 函数计算完返回值时,它“返回”的方式就是拿着返回值调用那个 Continuation。
你应该已经发现了,这也就是回调函数,我只是换了个名字而已。
异步的朴素实现:Callback
光有回调函数其实并没有卵用。对于纯粹的计算工作,Call Stack 就很好,为何要费时费力用回调来做 Continuation 呢?你说的对,但仅限于没有 IO 的情况。我们知道 IO 通常要比 CPU 慢上好几个数量级,在 BIO 中,线程发起 IO 之后只能暂停,然后等待 IO 完成再由操作系统唤醒。
var input = recv_from_socket() // Block at syscall recv() var result = calculator.calculate(input) send_to_socket(result) // Block at syscall send()
而异步 IO 中,进程发起 IO 操作时也会一并输入回调(也就是 Continuation),这大大解放了生产力——现场无需等待,可以立即返回去做其他事情。一旦 IO 成功后,AIO 的 Event Loop 会调用刚刚设置的回调函数,把剩下的工作完成。这种模式有时也被称为 Fire and Forget。
recv_from_socket((input) -> { var result = calculator.calculate(input) send_to_socket(result) // ignore result })
var promise_input = recv_from_socket() promise_input.then((input) -> { var result = calculator.calculate(input) send_to_socket(result) // ignore result })