# 异步编程(3):Generator函数
# 定义
- Generator 函数是一个状态机,封装了多个内部状态。执行Generator函数会返回一个遍历器对象 (opens new window),可以依次遍历Generator函数的每一个状态,
但是只有调用
next
方法才能遍历到下一个状态,所以其实提供了一种暂停执行函数,yield
表达式就是暂停标志。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
可以看出,Generator函数被调用时并不会执行
,而是返回了一个遍历器对象
,只有调用next
方法、内部指针指向该语句时才执行,每次调用next方法,就回返回一个对象包含value
和done
两个属性,value
属性表示内部状态的值,是yield表达式后面表达式返回的值;done
属性是个布尔值,表示是否遍历结束。
# Generator执行原理
# 协程原理
- Generator函数可以
暂停和恢复执行
,这就是协程的原理:一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程
。 - 一句话概括:
保存上下文,控制权切换……控制权恢复,取回上下文
。 - 协程的有两大优势:
没有线程切换开销,具有极高的执行效率
、不需要多线程的锁机制
:
# 协程与线程的关系
- 协程是一种比线程更加轻量级的存在。
- 普通线程是抢先式的,会争夺cpu资源,而协程是合作的,可以把协程看成是跑在线程上的任务。
- 一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。
关于协程
进程 > 线程 > 协程
协程的第一大优势是
没有线程切换开销,具有极高的执行效率
:因为子程序切换不是线程切换,而是由程序自身控制,因此没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;协程的第二大优势是
不需要多线程的锁机制
:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多;协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行,需要注意的是:在一个子程序中中断,去执行其他子程序,这并不是函数调用,有点类似于CPU的中断;
用汽车和公路举个例子:js公路只是单行道(主线程),但是有很多车道(辅助线程)都可以汇入车流(异步任务完成后回调进入主线程的任务队列);generator把js公路变成了多车道(协程实现),但是同一时间只有一个车道上的车能开(依然单线程),不过可以自由变道(移交控制权);
线程包含于进程,协程包含于线程,只要内存足够,一个线程中可以有任意多个协程,但某一个时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源;
就实际使用理解来说,协程允许我们写同步代码的逻辑,却做着异步的事,避免了回调嵌套,使得代码逻辑清晰;
何时挂起,唤醒协程:协程是为了使用异步的优势,异步操作是为了避免IO操作阻塞线程,那么协程挂起的时刻应该是当前协程发起异步操作的时候,而唤醒应该在其他协程退出,并且他的异步操作完成时;
单线程内开启协程,一旦遇到io,从应用程序级别(而非操作系统)控制切换对比操作系统控制线程的切换,用户在单线程内控制协程的切换,优点如下: 1)协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级; 2)单线程内就可以实现并发的效果,最大限度地利用cpu;
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
# 协程执行流程
它的运行流程大致如下:
1. 协程A开始执行
2. 协程A执行到某个阶段,进入暂停,执行权转移到协程B
3. 协程B执行完成或暂停,将执行权交还A
4. 协程A恢复执行
协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
# Generator的执行器
# 执行器概念
- 执行生成器代码的函数,就称为执行器,
co
模块就是一个著名的执行器。 - Generator 是一个异步操作的容器。它是需要手动执行的,它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点:
- 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
- Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权
# 实现基于 Promise 对象的简单自动执行器
下面代码中,只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。
function run(gen) {
var g = gen(); // 返回状态机
function next(data) {
var result = g.next(data);
if (result.done) {
return result.value;
} else {
result.value.then(() => {
next(data)
})
}
}
next()
}
这样使用:
function* foo() {
let res1 = yield fetch('https://xxx') //返回promise对象
console.log('res1', res1)
let res2 = yield fetch('https://xxx') //返回promise对象
console.log('res2', res2)
}
run(foo);