# 异步编程(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方法,就回返回一个对象包含valuedone两个属性,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 是一个异步操作的容器。它是需要手动执行的,它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点:
    1. 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
    2. 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);

参考链接:async/await 原理及执行顺序分析 (opens new window)

Last Updated: 4/24/2020, 1:44:36 PM