欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

与ES6生成器并发

程序员文章站 2022-07-13 08:42:06
...

与ES6生成器并发

如果您已经阅读并理解了本博客文章系列的第1部分第2部分第3部分 ,那么此时您可能会对ES6生成器充满信心。 希望您受到启发,能够真正突破极限,看看您能用它们做什么。

我们要探讨的最后一个主题是有点流血的东西,可能会使您的大脑有些扭曲(仍然扭曲着我的TBH)。 花些时间研究和思考这些概念和示例。 一定要阅读有关该主题的其他著作。

从长远来看,您在这里进行的投资将真正获得回报。 我完全相信,从这些想法中可以看出JS中先进的异步功能的未来。

正式的CSP(通信顺序过程)

首先,几乎完全由于David Nolen @swannodette的出色工作而使我对这个主题完全感到启发。 认真阅读他在该主题上写的任何内容。 以下是一些可帮助您入门的链接:

好,现在我来探讨这个话题。 我不是从Clojure的正式背景来学习JS的,也没有Go或ClojureScript的经验。 我发现自己很快就迷失了这些读物,我不得不进行大量实验并进行有根据的猜测,以从中收集有用的信息。

在此过程中,我认为我已经达成了具有相同精神,追求相同目标的目标,但是这是通过一种不太正式的思维方式实现的。

我试图做的是在保留(我希望!)大多数基本功能的同时,对Go风格的CSP(和ClojureScript core.async)API进行更简单的构建。 那些比我精明的人,很可能会很快发现我到目前为止在探索中错过的事情。 如果是这样,我希望我的探索会不断发展和进步,我将继续与您的读者分享这些启示!

分解CSP理论(一点点)

CSP到底是什么? 说“交流”是什么意思? “顺序”? 这些“过程”是什么?

首先,CSP来自Tony Hoare的书“ Communicationing Seequential Processes” 这是沉重的CS理论资料,但是如果您对事物的学术性感兴趣,那么这是最好的起点。 我绝不打算以笨拙,深奥的计算机科学方式解决这个问题。 我将非正式地讨论它。

因此,让我们从“顺序”开始。 这是您应该已经熟悉的部分。 这是谈论单线程行为和从ES6生成器获得的同步代码的另一种方式。

记住生成器如何具有如下语法:

function *main() {
    var x = yield 1;
    var y = yield x;
    var z = yield (y * 2);
}

这些语句中的每条语句都按顺序(顺序)执行,一次执行。 yield关键字注释了代码中可能发生阻塞暂停(仅在生成器代码本身而不是周围程序的意义上阻塞!)的点,但这并没有改变内部代码的自上而下处理的任何内容。 *main() 很容易,对吧?

接下来,让我们谈谈“流程”。 那是什么意思

本质上,生成器的行为类似于虚拟“进程”。 这是我们程序的一个独立部分,如果JavaScript允许这样的事情,它可以与程序的其余部分完全并行运行。

实际上,这会使事情有些混乱。 如果生成器访问共享内存(也就是说,如果生成器访问了其自己的内部局部变量之外的“*变量”),那么它就不是那么独立。 但是,让我们现在假设我们有一个不访问外部变量的生成器函数(因此FP理论将其称为“组合器”)。 因此,它在理论上可以按自己的过程运行。

因为这里的重要组成部分,具有两个或在一次去-但是,我们说,“过程” -复数。 换句话说,两个或更多的发电机配对在一起,通常可以合作完成一些更大的任务。

为什么要分离发电机而不是仅仅一个? 最重要的原因: 能力/关注点分离 如果您可以查看任务XYZ并将其分解为诸如X,Y和Z的子任务,则在其自己的生成器中实现每个任务往往会导致代码更易于推理和维护。

这与使用诸如function XYZ()函数之类的function XYZ()并将其分解为X()Y()Z()函数(其中X()调用Y()Y()调用Z()等。我们将函数分解为单独的函数,以更好地分离代码,这使代码更易于维护。

我们可以使用多个生成器来做同样的事情。

最后是“交流”。 那是什么意思 以上是合作的结果,即如果生成器要一起工作,则它们需要一个通信通道(不仅访问共享的周围词汇范围,而且它们都被授予互斥访问的真实共享通信通道) 。

这个沟通渠道会发生什么? 无论您需要发送什么(数字,字符串等)。 实际上,您甚至不需要通过通道实际发送消息即可通过通道进行通信。 “通信”可以像协调一样简单-就像将控制权从一个转移到另一个。

为什么要转移控制权? 主要是因为JS是单线程的,实际上在任何给定的时刻,只有其中一个可以活跃地运行。 其他人则处于运行暂停状态,这意味着他们处于任务的中间,但只是被暂停,等待在必要时恢复。

任意独立的“过程”可以神奇地协作和交流似乎并不现实。 松耦合的目标是令人钦佩的,但却不切实际。

取而代之的是,似乎CSP的任何成功实施都是对问题域现有的,众所周知的一组逻辑的有意分解,其中,每个部分都专门设计为与其他部分配合使用。

也许我对此完全错了,但是我还没有看到任何务实的方式可以将任意两个随机生成器函数轻松地粘合在一起成为CSP配对。 他们都需要被设计成可以与其他人一起工作,就通信协议达成一致,等等。

JS中的CSP

CSP理论在JS中有一些有趣的探索。

前面提到的David Nolen有几个有趣的项目,包括Om以及core.async Koa库(用于node.js)有一个非常有趣的方法,主要是通过use(..)方法实现的。 另一个非常忠实于core.async / Go CSP API的库是js-csp

您绝对应该检查那些很棒的项目,以查看各种方法以及如何在JS中探索CSP的示例。

异步runner(..) :设计CSP

因为我一直强烈地探索应用并发的CSP模式,以我自己的JS代码,这是一个自然的适合我致以异步流量控制的lib asynquence与CSP能力。

我已经有了runner(..)插件实用程序,该实用程序可以处理生成器的异步运行(请参阅“第3部分:使用生成器进行异步” ),因此我想到可以相当轻松地将其扩展为同时处理多个生成器。 以类似CSP的方式

我解决的第一个设计问题:您如何知道接下来要控制哪个发电机?

让每个人都有其他人必须知道的某种ID似乎太麻烦/笨拙,因此他们可以处理其消息或将控制权显式转移到另一个进程。 经过各种实验,我决定采用一种简单的循环调度方法。 因此,如果将三个生成器A,B和C配对,则A将首先获得控制权,然后B在A产生控制权时接管,然后C在B产生控制权时接管,然后A继续,依此类推。

但是,我们实际上应该如何转移控制权呢? 是否应该有一个明确的API? 再一次,经过多次实验,我决定采用一种更隐式的方法,这似乎(完全是偶然地)类似于Koa的方法 :每个生成器都引用一个共享的“令牌”- yield该信号将表示控制权转移。

另一个问题是消息通道的外观 一方面,您有一个相当正式的通信API,例如core.async和js-csp( put(..)take(..) )中的API。 经过自己的实验,我趋向于另一端,在这种情况下,一种不那么正式的方法(甚至没有API,只是一个像array这样的共享数据结构)似乎是足够的。

我决定使用一个数组(称为messages ),您可以根据需要任意决定如何填充/清空。 您可以push()消息推送到数组上,将pop()消息push()送到数组之外,按约定在数组中指定用于不同消息的特定插槽,在这些插槽中填充更复杂的数据结构,等等。

我的怀疑是,有些任务将需要真正简单的消息传递,而有些则要复杂得多,因此,除了使简单的情况变得不复杂之外,我选择不对消息通道进行array化以外的形式来对其进行形式化(因此,没有API,除了array本身)。 在您发现有用的情况下,很容易在消息传递机制上附加其他形式主义(请参见下面的状态机示例)。

最后,我观察到这些生成器“进程”仍然受益于独立生成器可以使用的异步功能 换句话说,如果您yield是Promise(或异步序列)而不是yield控制令牌,则Runner runner(..)机制确实会暂停以等待该将来的值,但不会转移控制权 -而是将结果值返回到当前进程(生成器),以便保留控制权。

最后一点可能是(如果我正确解释的话)最具争议性或与该领域的其他库不同。 真正的CSP似乎在这种方法上大吃一惊。 但是,我发现可以使用该选项非常非常有用。

一个愚蠢的FooBar示例

足够的理论。 让我们深入一些代码:

// Note: omitting fictional `multBy20(..)` and
// `addTo2(..)` asynchronous-math functions, for brevity

function *foo(token) {
    // grab message off the top of the channel
    var value = token.messages.pop(); // 2

    // put another message onto the channel
    // `multBy20(..)` is a promise-generating function
    // that multiplies a value by `20` after some delay
    token.messages.push( yield multBy20( value ) );

    // transfer control
    yield token;

    // a final message from the CSP run
    yield "meaning of life: " + token.messages[0];
}

function *bar(token) {
    // grab message off the top of the channel
    var value = token.messages.pop(); // 40

    // put another message onto the channel
    // `addTo2(..)` is a promise-generating function
    // that adds value to `2` after some delay
    token.messages.push( yield addTo2( value ) );

    // transfer control
    yield token;
}

好的,所以我们有两个生成器“进程”, *foo()*bar() 您会注意到它们都交给了token对象(当然,您可以随意调用它)。 token上的messages属性是我们的共享消息通道。 它从CSP运行的初始化开始就充满了传递给它的消息(请参见下文)。

yield token将控制权明确转移到“下一个”生成器(循环顺序)。 但是, yield multBy20(value)yield addTo2(value)都在产生promise(来自这些虚构的延迟数学函数),这意味着生成器会在此时暂停,直到promise完成。 根据承诺解决方案,当前处于控制状态的生成器将恢复运行并继续运行。

无论最终yield ed值是什么,在这种情况下yield "meaning of...表达式语句的yield "meaning of... CSP运行的完成消息(请参见下文)。

现在我们有了两个CSP流程生成器,我们如何运行它们? 使用异步

// start out a sequence with the initial message value of `2`
ASQ( 2 )

// run the two CSP processes paired together
.runner(
    foo,
    bar
)

// whatever message we get out, pass it onto the next
// step in our sequence
.val( function(msg){
    console.log( msg ); // "meaning of life: 42"
} );

显然,这是一个简单的例子。 但我认为它很好地说明了这些概念。

现在可能是一个不错的时机, 自己动手尝试一下 (尝试更改周围的值!),以确保这些概念有意义并可以自己编写代码!

另一个玩具演示示例

现在,让我们研究一个经典的CSP示例,但让我们从到目前为止所做的简单观察中,而不是从通常从其得出的学术纯粹主义者的角度进行研究。

乒乓球 多么有趣的游戏,是吧!? 这是我最喜欢的运动

假设您已经实现了用于打乒乓球的代码。 您有一个运行游戏的循环,并且有两段代码(例如, ifswitch语句中的分支)分别代表相应的玩家。

您的代码工作正常,您的游戏像乒乓冠军一样运行!

但是,对于CSP为什么有用,我在上面观察到了什么? 关注点/能力的分离。 我们在乒乓球比赛中有哪些独立功能? 两位选手!

因此,我们可以在很高的层次上用两个“进程”(生​​成器)为我们的游戏建模,每个玩家一个 当我们深入研究它的细节时,我们将意识到在两个玩家之间改组控制的“胶水代码”本身就是一项任务,并且代码可能在第三个生成器中,我们可以将其建模为游戏裁判

我们将跳过所有特定领域的问题,例如得分,游戏机制,物理,游戏策略,AI,控件等。我们在这里关心的唯一部分实际上是模拟来回ping(这实际上是我们对CSP控制传递的隐喻)。

想看演示吗? 现在运行它 (注意:使用最近有FF或Chrome的每晚版本,并支持ES6 JavaScript,以查看生成器的运行情况)

现在,让我们逐段看一下代码。

首先, 异步序列是什么样的?

ASQ(
    ["ping","pong"], // player names
    { hits: 0 } // the ball
)
.runner(
    referee,
    player,
    player
)
.val( function(msg){
    message( "referee", msg );
} );

我们用两个初始消息设置了序列: ["ping","pong"]{ hits: 0 } 我们一会儿再谈。

然后,我们设置了一个由3个进程(协程)组成的CSP运行: *referee()和两个*player()实例。

游戏结束时的最终消息将传递到我们序列中的下一步,然后作为裁判的消息输出。

裁判的执行:

function *referee(table){
    var alarm = false;

    // referee sets an alarm timer for the game on
    // his stopwatch (10 seconds)
    setTimeout( function(){ alarm = true; }, 10000 );

    // keep the game going until the stopwatch
    // alarm sounds
    while (!alarm) {
        // let the players keep playing
        yield table;
    }

    // signal to players that the game is over
    table.messages[2] = "CLOSED";

    // what does the referee say?
    yield "Time's up!";
}

我称控制令牌table与问题域匹配(乒乓球游戏)。 这是一个很好的语义,即当一名球员将球击回时,他会“屈服于对方”,不是吗?

只要他的秒表上的闹钟没有响起, *referee()while循环就只会将table退还给玩家。 完成后,他接管游戏,并用"Time's up!"宣布比赛结束"Time's up!"

现在,让我们看一下*player()生成器(我们使用它的两个实例):

function *player(table) {
    var name = table.messages[0].shift();
    var ball = table.messages[1];

    while (table.messages[2] !== "CLOSED") {
        // hit the ball
        ball.hits++;
        message( name, ball.hits );

        // artificial delay as ball goes back to other player
        yield ASQ.after( 500 );

        // game still going?
        if (table.messages[2] !== "CLOSED") {
            // ball's now back in other player's court
            yield table;
        }
    }

    message( name, "Game over!" );
}

第一个玩家将其名字从第一个消息的数组( "ping" )中"ping" ,然后第二个玩家将其名称( "pong" )从中"pong" ,以便他们都可以正确地标识自己。 两名球员还保留对共享ball对象的引用(带有其hits计数器)。

当球员尚未听到裁判员的闭幕消息时,他们通过增加hits计数器“击中” ball (并输出一条消息宣布该球),然后等待500毫秒(仅是为了假球移动)以光速!)。

如果游戏仍在进行,则他们“屈服桌子”给另一位玩家。

而已!

查看该演示的代码,以获取完整的上下文内代码清单,以查看所有部分的协同工作。

状态机:发电机协程

最后一个例子:将状态机定义为一组由简单助手驱动的生成协程。

演示 (请注意:使用FF或Chrome的最新版本,并支持ES6 JavaScript,以查看生成器的工作情况)

首先,让我们定义一个用于控制有限状态处理程序的助手:

function state(val,handler) {
    // make a coroutine handler (wrapper) for this state
    return function*(token) {
        // state transition handler
        function transition(to) {
            token.messages[0] = to;
        }

        // default initial state (if none set yet)
        if (token.messages.length < 1) {
            token.messages[0] = val;
        }

        // keep going until final state (false) is reached
        while (token.messages[0] !== false) {
            // current state matches this handler?
            if (token.messages[0] === val) {
                // delegate to state handler
                yield *handler( transition );
            }

            // transfer control to another state handler?
            if (token.messages[0] !== false) {
                yield token;
            }
        }
    };
}

这个state(..)帮助程序实用程序为特定的状态值创建了一个委托生成器包装器,该包装器自动运行状态机,并在每次状态转换时转移控制权。

纯粹按照惯例,我已经决定了共享token.messages[0]插槽将保存我们状态机的当前状态。 这意味着您可以通过传递来自上一个序列步骤的消息来播种初始状态。 但是,如果没有传递此类初始消息,则我们将默认默认为第一个定义的状态作为初始状态。 同样,按照惯例,最终的终端状态假定为false 您认为合适时,很容易更改。

状态值可以是您想要的任何类型的值: number s, string s等。只要可以使用===严格测试该值的相等性,就可以将其用于状态。

在以下示例中,我示出了状态机的四个之间的过渡number值的状态,在这个特定的顺序: 1 -> 4 -> 3 -> 2 仅出于演示目的,它还使用一个计数器,以便它可以多次执行转换循环。 当我们的生成器状态机最终达到终端状态( false )时, 异步序列就移到了下一步,正如您所期望的那样。

// counter (for demo purposes only)
var counter = 0;

ASQ( /* optional: initial state value */ )

// run our state machine, transitions: 1 -> 4 -> 3 -> 2
.runner(

    // state `1` handler
    state( 1, function*(transition){
        console.log( "in state 1" );
        yield ASQ.after( 1000 ); // pause state for 1s
        yield transition( 4 ); // goto state `4`
    } ),

    // state `2` handler
    state( 2, function*(transition){
        console.log( "in state 2" );
        yield ASQ.after( 1000 ); // pause state for 1s

        // for demo purposes only, keep going in a
        // state loop?
        if (++counter < 2) {
            yield transition( 1 ); // goto state `1`
        }
        // all done!
        else {
            yield "That's all folks!";
            yield transition( false ); // goto terminal state
        }
    } ),

    // state `3` handler
    state( 3, function*(transition){
        console.log( "in state 3" );
        yield ASQ.after( 1000 ); // pause state for 1s
        yield transition( 2 ); // goto state `2`
    } ),

    // state `4` handler
    state( 4, function*(transition){
        console.log( "in state 4" );
        yield ASQ.after( 1000 ); // pause state for 1s
        yield transition( 3 ); // goto state `3`
    } )

)

// state machine complete, so move on
.val(function(msg){
    console.log( msg );
});

应该很容易跟踪这里发生的事情。

yield ASQ.after(1000)表明这些生成器可以根据需要执行任何类型的基于yield ASQ.after(1000) / Sequence的异步工作,正如我们之前所看到的。 yield transition(..)是我们过渡到新状态的方式。

上面的state(..)助手实际上是在处理yield*委派和过渡变戏法的艰苦工作 ,使我们的状态处理程序以非常简单自然的方式表示。

摘要

CSP的关键是将两个或多个生成器“进程”结合在一起,为它们提供共享的通信通道,以及在彼此之间传递控制的方式。

在JS中,有许多库或多或少采用了相当正式的方法来匹配Go和Clojure / ClojureScript API和/或语义。 所有这些库背后都有真正聪明的开发人员,它们全都代表了进一步研究/探索的宝贵资源。

异步尝试采取一种不太正规的方法,同时希望仍然保留主要机制。 如果没有其他问题,当您进行实验和学习时, asynquence的Runner runner(..)使其非常容易开始使用类似CSP的生成器

不过,最好的部分是异步 CSP 与其其余的 其他异步功能 (承诺,生成器,流控制等) 协同工作。 这样一来,您就可以发挥出世界上最好的水平,并且可以在一个小的库中使用适合手头任务的任何工具。

现在,我们已经在最后四篇文章中详细探讨了生成器,我希望您很兴奋并受到启发,探索如何革新自己的异步JS代码! 您将使用发电机来构建什么?

与ES6生成器并发

关于凯尔·辛普森

凯尔·辛普森(Kyle Simpson)是一位开放网络传播者,对JavaScript充满热情。 他是作家,讲习班培训师,技术发言人和OSS贡献者/负责人。

github.com 帖子

翻译自: https://davidwalsh.name/concurrent-generators