1. 程式人生 > >PHP下的非同步嘗試三:協程的PHP版thunkify自動執行器

PHP下的非同步嘗試三:協程的PHP版thunkify自動執行器

PHP下的非同步嘗試系列

如果你還不太瞭解PHP下的生成器和協程,你可以根據下面目錄翻閱

  1. PHP下的非同步嘗試三:協程的Co自動執行器
  2. PHP下的非同步嘗試四:PHP版的Promise [待開發]

高階函式

在我們實現自動排程(器)函式前,我們先來理解下高階函式

thunk函式

# 先求值再傳參
function func(m){
  return m * 2;     
}

f(x + 5);

// 等同於

# 先傳參再求值
var thunk = function () {
  return x + 5;
};

function func(thunk){
  return thunk() * 2;
}

# 這段我們在python或一些語言裡,概念叫高階函式
# 因為php是解釋性動態語言,所以函式可以當引數傳入
# 這裡python,js,php下函式都是可以傳參的

PHP版本的thunkify函式

thunkify實現原理:

  1. 包裝一次原始函式名,然後返回一個第一次匿名函式(並攜帶包裝函式): return function () use ($func){$args = func_get_args();}
  2. 然後再獲取該匿名函式的引數,並在上一次第一次匿名函式體內返回一次帶回調引數的第二次匿名函式(並攜帶上一次環境上下文): return function ($callback) use ($args, $func){}
  3. 呼叫包裝函式,引數為:第一次匿名函式呼叫的引數+一個回撥函式
function thunkify($func){
    return function () use ($func) {
        $args = func_get_args();
        return function ($callback) use ($args, $func) {
            array_push($args, $callback);
            return $func(...$args);
        };
  };
};

$printStr = function($p1, $p2, $callback) {
    $callback($p1, $p2);
};

$printStrThunkify = thunkify($printStr);

$printStrThunkify(...["foo", "bar"])(function (...$p) {
    var_dump($p);
});

# output
array(2) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(3) "bar"
}

只能執行一次回撥的thunkify函式

function thunkify($func){
    return function () use ($func) {
        $args = func_get_args();
        return function ($callback) use ($args, $func) {
            // 原本的獲取引數,回撥會多次執行
            // array_push($args, $callback); 
            // 增加回調只能執行一次
            $callbackCalled = false;
            array_push($args, function (...$params) use ($callback, &$callbackCalled) {
                if ($callbackCalled) return ;
                $callbackCalled = true;
                $callback(...$params);
            });
            return $func(...$args);
        };
    };
};

$printStr = function($p1, $p2, $callback) {
    $callback($p1, $p2);
    $callback($p1, $p2); //我們增加一次回撥
};

$printStrThunkify = thunkify($printStr);

$printStrThunkify(...["foo", "bar"])(function (...$p) {
    var_dump($p);
});

# output
array(2) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(3) "bar"
}

看到這裡,你可能還在疑惑,thunkify函式其實只是幫我們包裝了一次有回撥函式的高階函式而已不過這裡到底有什麼用處呢,在普通場景下確實使用者不大(可能用處單純就在做一些前後置函式包裝也是用處的,類似python的裝飾)但是,但是,但是在生成器協程裡,Thunkify函式可以用於生成器協程的自動流程管理。

生成器協程的自動執行基礎理解

每一次yield出來的結果都是一個thunk函式的回撥

function thunkify($func){
    return function () use ($func) {
        $args = func_get_args();
        return function ($callback) use ($args, $func) {
            $callbackCalled = false;
            array_push($args, function (...$params) use ($callback, &$callbackCalled) {
                if ($callbackCalled) return ;
                $callbackCalled = true;
                $callback(...$params);
            });
            return $func(...$args);
        };
    };
};

$printStr1 = function($p1, $callback) {
    $callback($p1);
};
$printStr2 = function($p1, $callback) {
    $callback($p1);
};

$printStrThunkify1 = thunkify($printStr1);
$printStrThunkify2 = thunkify($printStr2);

function gen()
{
    global $printStrThunkify1, $printStrThunkify2;

    $r1 = yield $printStrThunkify1("1");
    var_dump($r1);
    $r2 = yield $printStrThunkify2("2");
    var_dump($r2);
}

$gen = gen();

// 手動回撥, 模擬自動執行基礎理解
$value = $gen->current();
$value(function ($p1) use($gen) {
    $value = $gen->send($p1);
    $value(function ($p1) use($gen) {
        $value = $gen->send($p1);
        var_dump($value);
    });
});

自動執行器

我們這裡只是實現上面的手動回撥執行增加了一個自動執行器,把生成器協程傳入後講自動執行生成器協程

function thunkify($func){
    return function () use ($func) {
        $args = func_get_args();
        return function ($callback) use ($args, $func) {
            $callbackCalled = false;
            array_push($args, function (...$params) use ($callback, &$callbackCalled) {
                if ($callbackCalled) return ;
                $callbackCalled = true;
                $callback(...$params);
            });
            return $func(...$args);
        };
    };
};

$printStr1 = function($p1, $callback) {
    sleep(2);
    $callback($p1);
};
$printStr2 = function($p1, $callback) {
    sleep(5);
    $callback($p1);
};

$printStrThunkify1 = thunkify($printStr1);
$printStrThunkify2 = thunkify($printStr2);

function gen()
{
    global $printStrThunkify1, $printStrThunkify2;

    $r1 = yield $printStrThunkify1("1");
    var_dump($r1);
    $r2 = yield $printStrThunkify2("2");
    var_dump($r2);
}

function autoCaller(\Generator $gen)
{
    // 注意這裡的$next use 引入作用域必須帶上&, 否則無法識別
    $next = function ($p1) use ($gen, &$next) {

        if (is_null($p1)) { //此處獲取第一次yeild的回撥
            $result = $gen->current();
        } else {
            // send後返回的是下一次的yield值
            $result = $gen->send($p1);
        }

        // 是否生成器迭代完成
        // 迭代器生成完成,不再迭代執行(自動執行器返回停止)
        if (!$gen->valid()) {
            return ;
        }

        $result($next);
    };

    $next(null);
}

$gen1 = gen();
//$gen2 = gen();

autoCaller($gen1);
//autoCaller($gen2);

# output
string(1) "1"
string(1) "2"

# 如果我們開啟上面的兩個sleep()註釋
# output

# 等待2秒
string(1) "1"
# 等待5秒
string(1) "2"

# 因為這裡我們的thunk裡執行的實際函式是同步的程式碼,所以整體是阻塞的後續程式碼執行的

總結

只要執行 autoCaller 函式,生成器就會自動迭代完成。這樣一來,非同步操作不僅可以寫得像同步操作,而且一行程式碼就可以執行。

Thunkify函式並不是 生成器協程 函式自動執行的唯一方案。

因為自動執行的關鍵是,必須有一種機制,自動控制 生成器協程 函式的流程,接收和交還程式的執行權。

回撥函式可以做到這一點,Promise 物件也可以做到這一點。本系列的下一篇,將介紹基於PHP的Promise實現的自動執行器。

附錄參考