sof3/await-generator

使用生成器在PHP中实现async/await

3.6.1 2023-07-27 09:45 UTC

README

Eng | |

await-generator

Build Status Codecov

一个用于在PHP中使用async/await模式的库。

文档

阅读await-generator教程,了解从生成器和传统的异步回调到await-generator的介绍。

为什么选择await-generator?

传统的异步编程需要回调,这导致 spaghetti 代码,被称为“回调地狱”

点击查看示例回调地狱
load_data(function($data) {
    $init = count($data) === 0 ? init_data(...) : fn($then) => $then($data);
    $init(function($data) {
        $output = [];
        foreach($data as $k => $datum) {
            processData($datum, function($result) use(&$output, $data) {
                $output[$k] = $result;
                if(count($output) === count($data)) {
                    createQueries($output, function($queries) {
                        $run = function($i) use($queries, &$run) {
                            runQuery($queries[$i], function() use($i, $queries, $run) {
                                if($i === count($queries)) {
                                    $done = false;
                                    commitBatch(function() use(&$done) {
                                        if(!$done) {
                                            $done = true;
                                            echo "Done!\n";
                                        }
                                    });
                                    onUserClose(function() use(&$done) {
                                        if(!$done) {
                                            $done = true;
                                            echo "User closed!\n";
                                        }
                                    });
                                    onTimeout(function() use(&$done) {
                                        if(!$done) {
                                            $done = true;
                                            echo "Timeout!\n";
                                        }
                                    });
                                } else {
                                    $run($i + 1);
                                }
                            });
                        };
                    });
                }
            });
        }
    });
});
使用await-generator,这可以简化为
$data = yield from load_data();
if(count($data) === 0) $data = yield from init_data();
$output = yield from Await::all(array_map(fn($datum) => processData($datum), $data));
$queries = yield from createQueries($output);
foreach($queries as $query) yield from runQuery($query);
[$which, ] = yield from Await::race([
    0 => commitBatch(),
    1 => onUserClose(),
    2 => onTimeout(),
])
echo match($which) {
    0 => "Done!\n",
    1 => "User closed!\n",
    2 => "Timeout!\n",
};

我能保持向后兼容吗?

是的,await-generator不对你的现有API施加任何限制。你可以将所有的await-generator调用作为内部实现细节包装起来,尽管强烈建议你直接公开生成器函数。

await-generator使用Await::f2c方法启动await上下文,你可以用它来适配到常用的回调语法

function oldApi($args, Closure $onSuccess) {
    Await::f2c(fn() => $onSuccess(yield from newApi($args)));
}

或者如果你想处理错误

function newApi($args, Closure $onSuccess, Closure $onError) {
    Await::f2c(function() use($onSuccess, $onError) {
        try {
            $onSuccess(yield from newApi($args));
        } catch(Exception $ex) {
            $onError($ex);
        }
    });
}

你可以继续调用以回调样式实现的函数,使用Await::promise方法(类似于JS中的new Promise

yield from Await::promise(fn($resolve, $reject) => oldFunction($args, $resolve, $reject));

为什么不选择await-generator?

await-generator有几个常见的陷阱

  • 忘记从Generator<void>方法中yield from将不会做任何事情。
  • 如果你从一个函数中删除所有的yield,由于PHP的魔法,它将自动变成一个非生成器函数。这个问题可以通过始终在函数签名中添加: Generator来缓解。
  • 如果异步函数从未解析(例如Await::promise(fn($resolve) => null)),则finally块可能永远不会执行。

虽然这些陷阱会导致一些麻烦,但await-generator风格仍然比回调地狱的bug更少。

那么纤维呢?

这可能是一个主观的观点,但我有几点不喜欢纤维的理由

类型签名中的显式挂起

fiber.jpg

例如,很容易从类型签名中看出$channel->send($value): Generator<void>会挂起,直到值被发送,而$channel->sendBuffered($value): void是一个非挂起的方法,会立即返回。类型签名通常是自解释的。

当然,用户可以调用sleep(),但对每个人来说都很明显,sleep()会阻塞整个运行时(如果他们不知道,当他们发现整个世界都停止时,他们就会知道)。

并发状态

当一个函数挂起时,许多其他事情都可能发生。确实,调用一个函数允许实现调用任何其他函数,这些函数可能会修改你的状态,但一个理智的、真正的HTTP请求的实现不会调用修改库的私有状态的函数。但是,由于纤维被抢占,其他纤维仍然可以修改私有状态。这意味着你必须每次调用任何可能挂起的函数时都检查私有属性可能的变化。

另一方面,使用显式await,挂起点很清楚,你只需要在已知的挂起点检查状态的变化。

捕捉挂起点

await-generator 提供了一个名为“捕获”的功能,允许用户向生成器添加预挂起和预恢复钩子。这通过向生成器添加一个适配器即可实现,甚至不需要 await-generator 运行时显式的支持。目前,fibers 不支持这一功能。