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 函数的具体应用如下:
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
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 函数体内输入数据。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
面代码中,第一个next方法的value属性,返回表达式x + 2的值3。第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的value属性,返回的就是2(变量y的值)。
部署错误处理代码
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出错了');
// 出错了
上面代码的最后一行,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try…catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
TOP3
异步任务的封装
下面看看如何使用 Generator 函数,执行一个真实的异步任务。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
面代码中,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 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。