21.5 生成器用作协同程序(多任务协作)

我们已经学习了生成器用作数据源和数据接收器。对于很多应用来说,最好严格区分这两个角色,因为这会使事情变得更加简单。本节描述了整个生成器接口(混合了两种角色),并在一个场景中两种角色都需要:多任务协作,任务既要能够发送信息,又要能够接收信息。

21.5.1 完整的生成器接口

完整的生成器对象接口, Generator ,既处理输出又处理输入:

interface Generator {
    next(value? : any) : IteratorResult;
    throw(value? : any) : IteratorResult;
    return(value? : any) : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

该接口在规范文档的“ Properties of Generator Prototype ”节描述了。

Generator 接口混合了两种之前章节见过的接口:用于输出的 Iterator 接口和用于输入的 Observer 接口。

interface Iterator { // data producer
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}

interface Observer { // data consumer
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

21.5.2 多任务协作

多任务协作场景需要生成器既能接收输入又能处理输出。在理解它的工作原理之前,先复习一下当前 JavaScript 中的并行方式。

JavaScript 运行在单进程中,有两种方式可以突破这个限制:

  • 多重处理( Multiprocessing ): Web Worker 使你可以在多个进程中运行 JavaScript 代码。数据共享是多重处理的最大陷阱。 Web Worker 通过不共享任何数据来避免这个问题。也就是说,如果你希望某个 Web Worker 能拿到数据,就必须传入数据的副本或者将数据转移给该 Web Worker (在这之后你再也不能在当前进程中访问该数据了)。
  • 多任务协作( Cooperative multitasking ):有大量的模式和库尝试处理多任务协作。运行多个任务的时候,在某一刻只能有一个任务执行。每一个任务必须显示地暂停自己,在任务切换的时候释放控制权。在这些尝试中,数据经常在任务之间共享。但是由于需要显示地暂停,就引入了一些风险。

有两种应用场景从多任务协作中受益,因为两种场景包含了通常按序执行的控制流,但是偶尔还是会暂停的。

  • Streams :任务会按序处理数据流,并且在没有数据的时候暂停。
    • 对于二进制数据, WHATWG 正致力于一个建议标准,该标准基于回调和 Promise 。
    • 对于数据流,通信顺序进程( CSP )是一个有趣的解决方案。基于生成器的 CSP 库在本章后续部分有讲述。
  • 异步计算 :在执行一个长时间运行的计算的时候,任务会被阻塞(暂停),直到计算完成。
    • 在 JavaScript 中, Promise 成为了处理异步计算的流行方式。 ES6 对其提供了支持。下节讲解了生成器是如何让 Promise 的使用变得简单。

21.5.2.1 通过生成器简化异步计算

几个基于 Promise 的库通过生成器简化异步代码。生成器用作 Promise 的接收器是非常完美的,因为生成器可以暂停,直到传入计算结果。

下面的例子展示了使用 T.J. Holowaychuk 的 co 库 之后写出来的代码的样子。我们需要两个库(如果通过 babel-node 运行 Node.js 代码)。

require('isomorphic-fetch'); // polyfill
let co = require('co');

co 是用于多任务协作的实际可用的库, isomorphic-fetch 是新的基于 fetch API ( XMLHttpRequest 的替代方案;阅读 Jake Archibald 的“ That’s so fetch! ”获取更多相关信息)的一个 polyfill 。 fetch 使得写一个返回指定 url 的文件的文本内容的函数 getFile 变得容易:

function getFile(url) {
    return fetch(url)
        .then(request => request.text());
}

我们现在具备了使用 co 的所有条件。下面的代码读取两个文件的文本内容,解析里面的 JSON 数据,并打印结果。

co(function* () {
    try {
        let [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('http://localhost:8000/croft.json'),
            getFile('http://localhost:8000/bond.json'),
        ]);
        let croftJson = JSON.parse(croftStr);
        let bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

注意这段代码看起来是多么漂亮的同步代码,即使在行 A 执行了异步调用。作为执行异步任务的生成器,通过 yield 传给整个任务流程的调度者 co 一个 Promise 对象。 yield 使得生成器暂停下来。一旦 Promise 返回结果,调度者 co 就会通过 next() 唤醒生成器,并传入结果。一个简单版本的 co 看起来像下面这样。

function co(genFunc) {
    let genObj = genFunc();
    run();

    function run(promiseResult = undefined) {
        let {value,done} = genObj.next(promiseResult);
        if (!done) {
            // A Promise was yielded
            value
            .then(result => run(result))
            .catch(error => {
                genObj.throw(error);
            });
        }
    }
}

21.5.3 基于生成器的多任务协作的限制

协同程序是没有限制的多任务协作程序:在协同程序内部,任何函数都可以暂停整个协同程序(函数自身的激活,函数调用者的激活,调用者的调用者,等等)。

相对而言,你只能在生成器函数的直接内部暂停该生成器,并且只能让当前的函数变为非活跃状态。由于这些限制,生成器有时又被叫做浅协同程序( shallow coroutines )。

21.5.3.1 生成器限制的好处

生成器的限制有两个主要的好处:

  • 生成器和事件循环兼容,后者在浏览器中提供了简单的多任务协作。我会马上讲解详细的内容。
  • 生成器实现起来相当简单,因为仅需要暂停一个活跃的函数,并且浏览器可以继续使用事件循环。

JavaScript 已经有一种非常简单的多任务协作方式:事件循环,安排任务队列中任务的执行。每个任务都开始于某个函数的调用,结束于该函数调用的完成。事件, setTimeout() 和其它一些机制添加任务到队列中。

这是事件循环的简单解释,对于目前而言足够了。如果你对细节感兴趣,参考关于异步编程的那一章。

这种方式的多任务确保了一件重要的事情:运行的完整性;每一个函数在执行完之前都不会被打断。函数成为了事务,可以执行完整的逻辑,不用暴露出来它们操作的处于某个中间状态的数据。并发访问共享数据使得多任务变得复杂起来,而在 JavaScript 的并发模型中这是不允许的。这就是为什么运行的完整性是一个优点。

然而,协同程序妨碍了运行的完整性,因为任何函数都可以暂停它的调用者。例如,下面的逻辑由多步组成:

step1(sharedData);
step2(sharedData);
lastStep(sharedData);

如果 step2 暂停了逻辑,其它任务就可以先于当前任务的最后一步执行。这些任务可能包含当前应用的其它部分,这些部分可以访问到处于未完成状态的 sharedData 。生成器保护了执行的完整性,它只暂停自身,然后返回调用者。

co 和类似的库提供了协同程序的强大能力,而摒弃掉了协同程序的缺陷:

  • 给任务提供了调度器,调度器通过生成器定义。
  • 任务“就是”生成器,因此可以被完全暂停。
  • 如果通过 yield* 嵌套调用(生成器)函数,那么该函数只具备暂停能力。调用者可以控制函数的暂停。

results matching ""

    No results matching ""