zoukankan      html  css  js  c++  java
  • 漫谈程序控制流

    作者: Liu, Martin 

    前言

    随着JavaScript最新版本号ECMAScript2015(ES6)的正式公布,以及babel这样的ES6ES5的工具的慢慢成熟, 在真实产品里使用ES6已经全然可行了。

    JS的朋友们,是时候点开es6features看一下了。

    值得一提的是。ES6特性里居然包含尾调用优化(tailcall),真是要点个赞。然而,这并没有什么用。

     

    ES6Generator谈起

    ES6众多新特性里。Generator无疑是一个非常酷的东西。

    cool在哪里?看code:

    // : 下面code能够在chrome dev toolconsole里直接跑

    function* fib() { // function*来定义generator

      var pre =0, cur =1;

      for (;;) { // 无限循环

        var temp = pre;

        pre = cur;

        cur += temp;

        var reset =yield cur; // yield决定next()的返回值

        if (reset){

        pre =0, cur =1;

        }

      }

    }

    var g =new fib();

    console.log(g.next().value); // 1

    console.log(g.next().value); // 2

    console.log(g.next().value); // 3

    console.log(g.next().value); // 5

    console.log(g.next(true).value); // 1

    在这里,我们定义了一个fibonacci数列的generator, 每次调用next(), yield下一个数字; next的參数则会是yield表达式的值,比方next(true),  yield cur就返回true

    能够看到code里有一个无限循环,但并不会导致CPU hang, 由于每一次yield后,程序会暂停,而在next()之后又继续执行。

    而这,便是generatorcool的地方:它改变了程序的控制流

     

    简单来说。yield停止,next继续,于是通过这个规则,我们就能够控制程序的执行顺序。

    于是我们能够写出这种code:

    co(function* (){

        var data1 =yield ajax_call_1(); // 发出一个ajax请求

        console.log(data1); // 输出response

        var data2 =yieldajax_call_2(data1); // 发出还有一个请求

        console.log(data2); // 输出response

        var data3 =yieldajax_call_3(data1); // 发出还有一个请求

        console.log(data3); // 输出response

        ...

    });

     

    //----------------------------------

    // 下面为对照的callback写法

    (function(){

      ajax_call_1(function(data1){

          console.log(data1);

          ajax_call_2(data1, function(data2){

              console.log(data2);

              ajax_call_3(data2, function(data3){

                  console.log(data3);

                  ...

              });

              ...

           });

        });

    })();

    Co那一段看似同步的代码实际上是异步运行的,可是直观简单美丽。写起来能够谈笑风生,比callback不知道高到哪去(关于这一点,能够看tj写的callbacks vscoroutines)

    至于那个co,就是某个奇怪的函数,在适当的时候(比方callback)调一下nextgenerator继续跑。感兴趣的话。请搜索co

     

    如今让我们总结一下: generator是一种特殊的子程序。而yield是一种流程控制指令,在generator里使用yield会将程序的控制权交还给调用者(即返回调用处),而外界调用generatornext方法会让该generator继续运行。generator能够用来做iterator,也能够用来玩魔法(同步转异步),由于它提供了一种较为优雅的流程控制方式

     

    只是。说是magic。但事实上程序的世界。并没有无根之木、无源之水。

    接下来,就让我们回溯本源,探一探各种流程控制结构的来龙去脉

    关于控制流(control flow)

    所谓控制流,说白了就是程序运行的顺序。

    我们知道,程序运行的基本原理是:cpuprogram counter(一个寄存器)拿指令的内存地址,然后去内存拿指令来运行,运行过程中会改变program counter的值(比方加1,也就是顺序运行)。如此循环往复直至结束。

    程序流程的控制,实际上就是在特定的情况下,更新特定的值到program counter。而上升到编程的层次,则是提供代表特定策略的流程控制语句,用以实现各种丰富的功能。

    一般而言。流程控制语句能够分为下面几类:

    • 无条件分支

    就是goto了。想去哪去哪(当然还是有限制的。比方c语言里goto就不能跳出当前function)。其缺点在于,程序可读性/可维护性easy变得极差。

    • 条件分支

    这个事实上就是大家耳熟能详的各种基本流程控制语句了,比方if-else, switch,以及for, whileloop语句

    • 无条件终止程序

    比方exit, return

    • 执行位在不同位置的一段指令。但完毕后会继续执行原来要执行的指令

    包含子程序(subroutine)、协程(coroutine)及延续性(continuation)。

    (注:generator实际上是一种coroutine)

    前三种比較直白简单。也比較常见,我们主要看第四种。

    执行还有一段指令。然后返回原指令段中继续执行

    这样的情况最常见的就是subroutine(比方function或者OO语言里的method)了。一般都是通过call stack来实现,每次function call都产生一个stack frame压入栈顶,该function结束时将其出栈,这样栈顶就变回其callerstack frame了,于是能够继续运行caller的代码。

    我们看一下典型的subroutine调用:

    function doOtherthing(){

        // block B

        {

            console.log("executing...doOtherthing");

            return;

        }

    }

     

    function doSomething(){

        // block A

        {

            console.log("executing...doSomething");

        }

     

        doOtherthing();

     

        // block C

        {

            console.log("continueexecuting...doSomething");

            return;

        }

    }

     

    doSomething();

    // executing...doSomething

    // executing...doOtherthing

    // continue executing...doSomething

    这里没什么奇怪的东西。分成几个block仅仅是为了方便引用,相信学过几天编程的都能理解。

    如今我们从控制流的角度分析一下这个程序。block B明显与AC不在一处,但运行顺序却是A=>B=>CA运行后是subroutine调用。jumpB处运行;而B运行后会返回到C处运行,这也正是return语句的语义。

    subroutine调用的运行顺序是固定的。这是由于return是一个keyword,提供隐式的流程控制。我们并不能像操作一个object一样来操作它——等等,假设能够呢?

    假设return的语义能够被抽象出来并能在程序中操作,那么我们将能够保存随意的运行点。而且在随意时候返回该处继续运行。

    这句话的意思是,我们的程序将能够实现随意的控制流。而无需运行环境的支持,比方说我们能够在ES5里实现generator

    你也许已经听过这样的抽象的名字:continuation

    Continuation

    在计算机科学里,Continuation是指能够被编程语言訪问的、对程序控制流程/状态的抽象表示。简单来说,就是程序执行时的某个执行点,比方上文所说的block B里的return执行后的那个点。

    return语句能够理解为隐式的调用了currentcontinuation

     

    我们说current continuation, 是指在那个点之后将要运行的代码,比方B return时。currentcontinuation就是整个block C

     

    说到continuation,就不得不说CPS(continuationpassing style),顾名思义,就是显式的将continuation作为參数传递,以此来进行流程控制。

    而我们寻常写的code叫做direct style,比方上文doSomething那段code

    我们如今将之前的code改写成CPS:

    functiondoOtherthing(k){

        // block B

        {

            console.log("executing...doOtherthing");

            k();

        }

    }

     

    function doSomething(k){

        // block A

        {

            console.log("executing...doSomething");

        }

     

        doOtherthing(function(ret){

            // block C

            {

                console.log("continueexecuting...doSomething");

                k();

            }

        });

    }

     

    doSomething(function(){});

    // executing...doSomething

    // executing...doOtherthing

    // continue executing...doSomething

    改写后运行结果是一样的。能够看到函数调用变成了callback的形式,而return都变成了k()。这个k就是传入的continuation

     

    注意。这里的重点并不在于callback形式。而在于CPS变换,之前的code和这段code是等价的。在这里我们是手动做的CPS变换,但实际上,全部direct stylecode都能够被自己主动变换成CPScode(至于怎么变换。能够尝试看How to compile with continuations)

     

    为何要强调自己主动的CPS变换?由于它能够用来实现一个瑞丽无匹的强大函数call/cc

    call/cc

    scheme语言里有一个著名的函数,叫做call-with-current-continuation, 一般简称为call/cc

    call/cc接受一个函数作为參数。并捕捉current continuation然后将之传递给这个函数。而continuation一被调用,call/cc马上返回,返回值即为传给continuation的參数。
    比方:

    (let ((a (call/cc

              (lambda (k)

                (begin

                  (display"will execute ") ; 输出 "will execute "

                  (k 1)

                  (display"will not execute")))))) ; 不运行

      (display a)) ; 输出1

    再来段JS的版本号,JS里当然是没有call/cc的了。只是这最好还是碍我用JS来表达。此处如果js里有一个等价的callcc

    var a = callcc(function(k){

        console.log('will execute'); // 输出 "will execute"

        k(1);

        console.log('will not execute'); // 不会运行

    });

     

    console.log(a); // 输出1

    这段code等同于:

    (function(k){

        console.log('will execute'); // 输出 "will execute"

        k(1);

    })(function(a){

        console.log(a);

    });

    这实质上就是自己主动做了CPS变换。

    有了call/cc。就能够直接在程序里操作continuation了。仅从功能上来说,就能够实现各种高级控制流,而不须要编译器/解释器这个level的支持了。

    接下来就让我们看看call/ccpower

    Coroutine

    Coroutine(协程)是一种类似subroutine但更灵活的控制结构。它同意有多个程序进入点,能够任意暂停继续运行,主要用来做nonpreemptivemultitasking(非抢占式多任务处理)

     

    coroutine中,能够通过yield语句来转移控制权,比方两个coroutinec1c2。在c1中调用yield to c2(伪代码), 就会去运行c2,在c2中又调用yield to c1,就会继续运行c1(又一次进入之前的运行点)

    听起来和之前说的generator有些像?事实上generator就是一种coroutine。我们之后会讲到。

     

    Donald Knuth说:"subroutinecoroutine的特例",由于subroutine能够看作不使用yieldcoroutine

     

    我们说coroutine是用来做nonpreemptive multitasking(非抢占式多任务处理)的。要理解这一点,最好先理解preemptivemultitasking(抢占式多任务处理)

    我们熟知的基于多线程的多任务/并发处理就是preemptive, 程序控制权由调度器而非程序自身来决定。实质上就是在程序外部强行打断程序的执行,再依据某种策略(优先级,动态时间片)决定由哪个线程继续执行。

    coroutine是自已决定将控制权交给谁(yield)。因而不会有race condition, 不须要考虑锁的问题,能够极大的简化并发编程。

    这里要提一下为何我们熟知的是抢占式多任务处理,由于人们须要流畅的同一时候做多件(不相关的)事的能力,比方在上网时下载和听音乐,而抢占式的多任务处理有助于实现这一点(不会由于控制权被占用而导致其他应用hang)。而编程时关注的是怎样更高效的做好事情,而且开发人员知晓所有的context。也就easy明确怎样去协调控制权,所以从编程的角度。非抢占式多任务处理反而更有优势。

    而从详细实现的角度来看,coroutine通常是语言级别的实现,实际上是在用户态进行上下文切换,不会陷入内核态,因而更高效。

    coroutine的缺点是无法利用多核。它仅仅能做concurrency,而不能做parallelism,由于一般它是跑在一个线程上。多个coroutine不能同一时候执行。可是也有改进的方案。比方go语言的goroutine, 就是work在一个线程池之上的。只是这样就须要更复杂的调度了,当然名字也华丽的变了。

    下面是用callcc实现简单的协程(只是不能执行):

    var queue = [];

    function isEmpty(){

        return queue.length==0;

    }

    function enqueue(x){

        queue.push(x);

    }

    function dequeue(){

        return queue.shift();

    }

    function run(f){

        callcc(function(k){

            enqueue(k);  // current continuation enqueue

            f();

        });

    }

    function $yield(){

        callcc(function(k){

            enqueue(k);

            dequeue()(); // dequeue某个continuation并运行

        });

    }

     

    functiondoSomething(str){

        for(;;) {

            console.log(str);

            $yield(); // 放弃控制权

            // point C

        }

    }

     

    run(function(){

        doSomething("A");

    });

     

    // point A

    run(function(){

        doSomething("B");

    });

     

    // point B

    if(!isEmpty){

        dequeue()();

    }

     

    // 理论上的输出结果为

    // A

    // B

    // A

    // B

    // ..., AB交替输出

    简单描写叙述一下程序的运行:

    1. 运行第一个run
      • continuation指向point A。然后它被enqueue, 此时queue为[A]
    2. 运行doSomething("A")
      • 输出A
      • 运行$yield()
        • 将当前continuation enqueue, 此时queue为[A, C-A]
        • dequeue并运行。此时queue为[C-A], 从point A处运行
    3. point A, 运行第二个run
      • continuation指向point B, 然后它被enqueue。此时queue为[C-A, B]
    4. 运行doSomething("B")
      • 输出B
      • 运行$yield()
        • 将当前continuation enqueue, 此时queue为[C-A, B, C-B]
        • dequeue并运行,此时queue为[B, C-B], 从C-A处运行
    5. C-A处
      • 输出A
      • 运行$yield()
        • enqueue, queue为[B, C-B, C-A]
        • dequeue并运行。此时queue为[C-B, C-A], 从B处运行
    6. B处,dequeue并运行。从C-B处运行,输出B,并enqueue, 此时queue为[C-A, C-B]
    7. [C-A, C-B] -> [C-B, C-A] -> [C-A, C-B] 循环, A和B交替输出

    可见通过callcc和一个queue。我们能够轻易的实现coroutine

    Generator

    Generator又叫Semi-Coroutine(半协程)Asymmetric Coroutine(不正确称协程),它本质上仍是协程,和一般的协程的差别在于,generator仅仅能把控制权交还给它的caller, coroutine是能够决定把控制权交给谁。

    先看一个callcc实现的generator:

    function fib(){

        var controlState =function($yield){

            var pree =0;

            var pre =1;

            while (true){

                callcc(function(resume){

                    controlState = resume;

                    $yield(pree);

                });

                var tmp = pree;

                pree = pre;

                pre = tmp + pre;

            }

        };

     

        return {

            next:function(){

                return callcc(controlState);

            }

        };

    }

    next调用时,进入controlState函数。$yield一旦调用,立即返回,可是controlState已经被替换成内部循环处的continuation,因而当next再调用时会回到循环处继续运行。

     

    事实上generator能够和coroutine互相转化,由于它们本质上是一样的东西。generator加一个scheduler就能够实现coroutine(yield一个value, 然后依据value决定resume哪个generator)

    来一个用ES6 generator模拟couroutine的样例(能够在chrome dev tool里执行):

    function* ge1(){

        for (var i =1; ;i++){

            console.log('running...generator1, '+ i +' times');

            yield'g2';

        }

    }

     

    function* ge2(){

        for (var i =1; ;i++){

            console.log('running...generator2, '+ i +' times');

            yield'g1';

        }

    }

     

    function schedule(){

        var map = {

            'g1': ge1(),

            'g2': ge2()

        };

        var current ='g1';

        for(var i =0; i <100; i++) {

            current = map[current].next().value; // current 'g1''g2'间来回变化

        }

    }

     

    schedule();

    Delimited Continuation

    关于continuation, 另一个值得一提的是DelimitedContinuationScala里就支持这样的continuation

    它是在一个限定的区域里,捕捉continuation并详细化成一个函数。以供复用。通常是通过reset+shift来表达:

    (reset

     (display (*2 (shift k

                          (k 2)

                          (k 4)

                          (k 3)))))

    JS来翻译一下:

    reset(function(){

        var x =shift(function(k){

            k(2);

            k(4);

            k(3);

        });

        console.log(2* x);

    });

    // 4

    // 8

    // 6

    再翻译成CPS

    (function(k){

        k(2);

        k(4);

        k(3);

    })(function(x){

        console.log(2* x);

    });

    如今应该非常好理解了,类似于call/cc的情况。只是用reset限定了一个scopeshift之后reset之内的代码捕捉并封装成一个函数,然后传递给shift块里的那个函数。

    这里一个要注意的点是。shift里的k是一个函数。所以它能够多次使用;而call/cc里的k调用一次就退出了,后边的都会ignore

    从这个角度而言。delimitedcontinuation是纯粹的函数,而undelimited continuation不是。因而delimited continuation更直观更符合直觉,也就更适合我们用来编程

    最后

    碍于能力以及篇幅,本文仅是对程序控制流的浅尝辄止。

    纵观而言。各种高阶的流程控制结构,都与continuation相关,这是由于continuation是对(隐式的)控制流本身的抽象。

    只是现代的高级语言里。一般不直接提供first-classcontinuation, 而是提供如generator,coroutine甚至delimited continuation等的高阶控制结构,由于它们足够强大而又相对call/cc更可控更易于理解。

    而它们的实现也不会是像我这里所写的那样简单,甚至也不一定是基于call/cc去实现。然而其基本原理是一致的。因此理解continuation, 理解CPS,理解call/cc,将有助于更好的玩转各种流程控制。

    References:

    https://github.com/lukehoban/es6features

    https://medium.com/@tjholowaychuk/callbacks-vs-coroutines-174f1fe66127

    https://en.wikipedia.org/wiki/Control_flow

    https://en.wikipedia.org/wiki/Continuation

    https://en.wikipedia.org/wiki/Continuation-passing_style

    https://en.wikipedia.org/wiki/Generator_(computer_programming)

    https://en.wikipedia.org/wiki/Coroutine

    https://en.wikipedia.org/wiki/Delimited_continuation
  • 相关阅读:
    JDBC 复习4 批量执行SQL
    JDBC 复习3 存取Oracle大数据 clob blob
    Oracle复习
    Linux命令(1)grep
    JDBC 复习2 存取mysql 大数据
    JDBC 复习1 DBUtil
    php 环境搭建问题
    Windows 批处理 bat 开启 WiFi 菜单选项 设置ID PWD
    Bat 批处理启动和停止Oracle 服务
    docker 学习1 WSL docker ,Windows docker
  • 原文地址:https://www.cnblogs.com/liguangsunls/p/7347012.html
Copyright © 2011-2022 走看看