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

PHP下的異步嘗試系列

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

  1. PHP下的異步嘗試一:初識生成器
  2. PHP下的異步嘗試二:初識協程
  3. PHP下的異步嘗試三:協程的Co自動執行器
  4. 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實現的自動執行器。

附錄參考

Thunk 函數的含義和用法 - 阮一峯

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章