TOP1

在谈线程和进程

在上个章节我们学到了 Promise 也了解了 Promise 所做的就是改变了传递回调的地方。 通过中立 Promise 机制给异步操作提供了控制反转的能力,在安全性上直接进行加固。但在某些复杂的异步操作情况下,使用promise你甚至需要写更多的回调。也仍然需要把 Promise 嵌套在别的 Promise 中!

所以我们有需要一种更好的异步流程方式,来组织管理代码。 今天我们要来学习的 Generator 给了我们一种新思路。

在讲Generator 之前我们需要再来聊聊线程和进程。

进程是什么,我们都知道,一个程序的运行的实例。早年的Windows 3.x 系统对于多进程切换 是通过 非抢夺式,也叫协作式多任务的方式

这种多任务方式存在一个很大的弊端,那就是必须等一个进程主动让出执行权,其它进程才有机会执行。

如果当前进程不让(比如陷入死循环、比如调用非阻塞API循环死等总是不来的网络报文、比如用错误的接口循环死等读取硬件故障的磁盘),那么整个系统就会陷入瘫痪。

从Windows 95开始,微软切换到了抢夺式多任务:每个进程给你一个时间片,用完就强制休眠;甚至时间片未到但更紧急的事件出现了,也会强制低优先级进程休眠。

抢夺式多任务的确比非抢夺式多任务可靠得多。一个经验不足的程序员写的程序把进程搞奔溃了,其进程可以不受影响。系统会强制剥夺它的执行权,确保正常程序仍然有机会执行。

Windows 95也算是一个里程碑,它标志着一个真正支持多进程的操作系统出现了。

无论是协作式多任务还是抢夺式多任务,外在表现上都是“用户可以同时运行多个程序,可以开着微信,放着音乐,然后一边在打着游戏……”

随着计算机技术的发展,多CPU系统越发普及,那么这时候,我们就会想到很多很酷的应用场景。比如说,可以一边从网上下载电影,一边开个视频播放器观看。

但这样就出现了很多新问题:电影下载进程下了100k数据,一校验,是坏的。结果视频播放器快手快脚拿过去就放;刚放了一帧,下载进程作废了这段数据,把读指针跳回到100k之前;此时就播放器奔溃了? 搞什么我还保留着一个无效的读指针……

这时候,开发人员就不得不做很多的同步工作。

比如得先约定共享内存/共享文件格式,约定控制数据存储位置,约定锁是读写锁还是其他什么所等等…

除非同一个团队做,不然想要配合默契,显然是极难极难的。
但既然是同一个团队做,就没必要搞成两个进程,动用复杂的进程间通讯机制。 为了更加充分的利用硬件资源,操作系统也可以允许一个进程存在两个执行绪。

于是,线程诞生。

有了进程设计的经验,从一开始,操作系统就汲取了过去的教训,把线程也做成了抢夺式多任务。 进程和线程都要在系统里面注册,这才能接受系统的调度。

进程和线程这两者的区别是,进程持有资源,一旦退出,进程申请的各种资源都会被系统强制回收;而线程依附于进程,资源不和它绑定。

但是抢夺式多任务也会存在一些问题就是执行时序紊乱

假设一个场景我有两个线程 A 线程负责从网上下载内容, B线程负责播放。 A线程的任务就是每下载一段、校验没问题,它就要更新一下共享内存。如果不存在 抢夺,那么什么时候它把内容更新完了,什么时候主动让出控制权,那就不会有任何问题。

但一旦存在抢夺,A线程就可能在刚刚执行到 “修改了链表的末指针、但尚未来得及修改最后一块的前向指针“时,被系统强制剥夺执行权;而 B 线程负责播放,它刚读到这块信息,用户点了回退,于是它循着A线程尚未来得及修改正确的前向指针,这个时候就发生了播放器错误的消息。

因此,我们不得不在使用共享数据时加锁,确保不会出现类似的事情。

此时,非抢占式多任务的好处就出来了,当 事情没做完而且并不会耽误太久时,你们就应该等我;而一旦我事情做完了、或者需要等待网络信号/磁盘准备好时,也会痛快的主动交出控制权。

这个做法,使得协作式多任务之间执行权的交接点极为明晰;那么只要逻辑考虑清楚了,锁就是完全没必要的。反正不会抢夺,事情没结束前我就不交执行权;交执行权之前确保不存在 “悬置的、未确定未提交的修改”,这样 脏读脏写就杜绝了。

因此,协程这个概念的提出,使得程序逻辑更为清晰,执行更加可控。协程实质上是一种在用户层面实现的协作式多线程架构。

协程,的协字有着协助 协作的意思,协助谁协助线程的执行。 注意了:协程最初的设计是不会让系统知道自己的存在的,如果系统一但知道协程的存在,那么它就会把协程当线程调度,于是抢占式多任务就又回来了。

脱离了抢占式调度的束缚,协程的优势就体现出来了每次执行中,协程之间的具体执行顺序可能千变万化;但协程执行权切换只会发生在用户明确放弃执行权之后——比如你明确执行了yield语句时。 这样我们就能做到我任务没完成我是不会放弃执行权的。

有了以上的基础概念之后现在我们就去了解下 Generator 是什么? 以及他给 JavaScript 异步编程带来了什么。

TOP2

协程的 Generator 函数实现

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用 yield 语句注明。
Generator 函数的具体应用如下:

  1. function* gen(x) {
  2. var y = yield x + 2;
  3. return y;
  4. }
  5. var g = gen(1);
  6. g.next() // { value: 3, done: false }
  7. g.next() // { value: undefined, done: true }

上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针 g 的 next 方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield 语句,上例是执行到 x + 2 为止。

换言之,next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息 这个对象中有两个属性 value 和 done 属性。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

Generator 函数的数据交换和错误处理

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

next返回值的 value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

  1. function* gen(x){
  2. var y = yield x + 2;
  3. return y;
  4. }
  5. var g = gen(1);
  6. g.next() // { value: 3, done: false }
  7. g.next(2) // { value: 2, done: true }

面代码中,第一个next方法的value属性,返回表达式x + 2的值3。第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的value属性,返回的就是2(变量y的值)。

部署错误处理代码

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

  1. function* gen(x){
  2. try {
  3. var y = yield x + 2;
  4. } catch (e){
  5. console.log(e);
  6. }
  7. return y;
  8. }
  9. var g = gen(1);
  10. g.next();
  11. g.throw('出错了');
  12. // 出错了

上面代码的最后一行,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try…catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

TOP3

异步任务的封装

下面看看如何使用 Generator 函数,执行一个真实的异步任务。

  1. var fetch = require('node-fetch');
  2. function* gen(){
  3. var url = 'https://api.github.com/users/github';
  4. var result = yield fetch(url);
  5. console.log(result.bio);
  6. }

面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。

执行这段代码的方法如下。

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。