给予 PHP 「async/await 等待式异步」(代码流控制)设计模式的程序库。
建议先阅读 await-generator 教学(中文版赶工中),它涵盖了生成器、传统「回调式异步」,再到 await-generator 等概念的介绍。
以下部分名词在 await-generator 教学中都更详细地讲解(「回调」等)。
传统的异步代码流需要靠回调(匿名函数)来实现。 每个异步函数都要开新的回调,然后把异步函数后面的代码整个搬进去,导致了代码变成「callback hell 回调地狱」,难以被阅读、管理。
点击以查看「回调地狱」例子
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);
}
});
};
});
}
});
}
});
});
$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 不会对已有的接口造成任何限制。 你可以将所有涉及 await-generator 的代码封闭在程序的内部。 但你确实应该把生成器函数直接当作程序接口。
await-generator 会在 Await::f2c
开始进行异步代码流控制,你可以将它视为「等待式」至「回调式」的转接头。
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
method 转化成「等待式」。
它跟 JavaScript 的 new Promise
很像:
yield from Await::promise(fn($resolve, $reject) => oldFunction($args, $resolve, $reject));
await-generator 也有很多经常坑人的地方:
- 忘了
yield from
的代码会毫无作用; - 如果你的函数没有任何
yield
或者yield from
, PHP 就不会把它当成生成器函数(在所有应为生成器的函数类型注释中加上: Generator
可减轻影响); - 如果异步代码没有全面结束,
finally
里面的代码也不会被执行(例:Await::promise(fn($resolve) => null)
);
尽管一些地方会导致问题, await-generator 的设计模式出 bug 的机会依然比「回调地狱」少 。
虽然这样说很主观,但本人因为以下纤程缺少的特色而相对地不喜欢它:
先生,你已在暂停的纤程待了三十秒。
因为有人实现一个界面时调用了Fiber::suspend()
。
好家伙,我都等不及要回应我的 HTTP 请求了。
框架肯定还没把它给超时清除。
例如能直观地看出 $channel->send($value): Generator<void>
会暂停代码流至有数值被送入生成器; $channel->sendBuffered($value): void
则不会暂停代码流,这个 method 的代码会在一次过执行后回传。
类型注释通常是不言自明的。
当然,用户可以直接调用 sleep()
,但大家都应清楚 sleep()
会卡住整个线程(就算他们不懂也会在整个「世界」停止时发现)。
当一个函数被暂停时会发生许多其他的事情。 调用函数时固然给予了实现者调用可修改状态函数的可能性, 但是一个正常的、合理的实现,例如 HTTP 请求所调用的函数不应修改你程序库的内部状态。 但是这个假设对于纤程来说并不成立, 因为当一个纤程被暂停后,其他纤程仍然可以修改你的内部状态。 每次你调用任何可能会被暂停的函数时,你都必须检查内部状态的可能变化。
await-generator 相比起纤程,异步、非异步代码能简单区分,且暂停点的确切位置显而易见。 因此你只需要在已知的暂停点检查状态的变化。
await-generator 提供了一个叫做「捕捉」的功能。 它允许用户拦截生成器的暂停点和恢复点,在它暂停或恢复前执行一段加的插代码。 这只需透过向生成器添加一个转接头来实现。甚至不需要 await-generator 引擎的额外支援。 这目前在纤程中无法做到。