Guidelines
这可能是诱人的,只要将async
,await
和Awaitable
放在你所有的代码里。虽然可以有更多的async
功能 - 事实上,你一般不应该害怕做一个功能,async
因为没有性能损失这样做 - 有一些准则,你应该遵循,以最大限度地发挥有效利用async
。
Be Liberal, but Careful, with Async
如果你正在努力为您的代码是否应该是Async与否,通常可以开始寻找答案肯定,并找到一个理由说没有。例如,一个简单的hello world程序可以用Async处理,没有性能损失。您可能无法获得任何收益,但您不会收到任何损失 - 它将针对任何可能需要Async的更改进行设置。
这两个程序是为了所有意图和目的,等同的。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\NonAsyncHello;
function get_hello(): string {
return "Hello";
}
function run_na_hello(): void {
var_dump(get_hello());
}
run_na_hello();
Output
string(5) "Hello"
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\Hello;
async function get_hello(): Awaitable<string> {
return "Hello";
}
async function run_a_hello(): Awaitable<void> {
$x = await get_hello();
var_dump($x);
}
run_a_hello();
Output
string(5) "Hello"
只要确保你遵循其余的准则。Async非常好,但您仍然需要考虑缓存,批量和效率等方面。
使用Async扩展
对于Async将提供最大效益的常见情况,HHVM提供方便的扩展库,以帮助编写代码更容易。根据您的用例情况,您应该自由使用:
不要在循环中使用Async
如果您只记住一条规则,请记住:
**不要await循环**
它完全违反了Async的目的。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\Loop;
class User {
public string $name;
protected function __construct(string $name) { $this->name = $name; }
static function get_name(int $id): User {
return new User(str_shuffle("ABCDEFGHIJ") . strval($id));
}
}
async function load_user(int $id): Awaitable<User> {
// Load user from somewhere (e.g., database).
// Fake it for now
return User::get_name($id);
}
async function load_users_await_loop(array<int> $ids): Awaitable<Vector<User>> {
$result = Vector {};
foreach ($ids as $id) {
$result[] = await load_user($id);
}
return $result;
}
function runMe(): void {
$ids = array(1, 2, 5, 99, 332);
$result = \HH\Asio\join(load_users_await_loop($ids));
var_dump($result[4]->name);
}
runMe();
Output
string(13) "JFHBIAEDGC332"
在上面的例子中,循环正在做两件事情:
- 使循环迭代成为如何运行代码的限制因素。通过循环,您可以确保顺序获取用户。
- 您正在创建错误的依赖关系。加载一个用户不依赖于加载另一个用户。
相反,您将需要使用我们的Async感知映射功能vm()。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\NoLoop;
class User {
public string $name;
protected function __construct(string $name) { $this->name = $name; }
static function get_name(int $id): User {
return new User(str_shuffle("ABCDEFGHIJ") . strval($id));
}
}
async function load_user(int $id): Awaitable<User> {
// Load user from somewhere (e.g., database).
// Fake it for now
return User::get_name($id);
}
async function load_users_no_loop(array<int> $ids): Awaitable<Vector<User>> {
return await \HH\Asio\vm(
$ids,
fun('\Hack\UserDocumentation\Async\Guidelines\Examples\NoLoop\load_user')
);
}
function runMe(): void {
$ids = array(1, 2, 5, 99, 332);
$result = \HH\Asio\join(load_users_no_loop($ids));
var_dump($result[4]->name);
}
runMe();
Output
string(13) "AJBIHCDGFE332"
考虑到数据依赖性很重要
学习如何构建Async代码最重要的方面是理解数据依赖关系模式。以下是如何确保Async代码是数据依赖性的一般流程:
- 将每个没有分支(链)的依赖关系序列放入其自己的async函数中。
- 将每条并行链捆绑到其自己的async功能中。
- 重复一下,看看是否进一步减少。
假设我们正在收到作者的博客文章。这将涉及以下步骤:
- 获取作者的帖子ID。
- 获取每个帖子ID的帖子。
- 获取每个帖子ID的评论数。
- 生成最后一页信息
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\DataDependencies;
// So we can use asio-utilities function vm()
class PostData {
// using constructor argument promotion
public function __construct(public string $text) {}
}
async function fetch_all_post_ids_for_author(int $author_id)
: Awaitable<array<int>> {
// Query database, etc., but for now, just return made up stuff
return array(4, 53, 99);
}
async function fetch_post_data(int $post_id): Awaitable<PostData> {
// Query database, etc. but for now, return something random
return new PostData(str_shuffle("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
}
async function fetch_comment_count(int $post_id): Awaitable<int> {
// Query database, etc., but for now, return something random
return rand(0, 50);
}
async function fetch_page_data(int $author_id)
: Awaitable<Vector<(PostData, int)>> {
$all_post_ids = await fetch_all_post_ids_for_author($author_id);
// An async closure that will turn a post ID into a tuple of
// post data and comment count
$post_fetcher = async function(int $post_id): Awaitable<(PostData, int)> {
list($post_data, $comment_count) =
await \HH\Asio\v(array(
fetch_post_data($post_id),
fetch_comment_count($post_id),
));
/* The problem is that v takes Traverable<Awaitable<T>> and returns
* Awaitable<Vector<T>>, but there isn't a good value of T that represents
* both ints and PostData, so they're currently almost a union type.
*
* Now we need to tell the typechecker what's going on.
* In the future, we plan to add HH\Asio\va() - VarArgs - to support this.
* This will have a type signature that varies depending on the number of
* arguments, for example:
*
* - va(Awaitable<T1>, Awaitable<T2>): Awaitable<(T1, T2)>
* - va(Awaitable<T1>,
* Awaitable<T2>,
* Awaitable<T3>): Awaitable<(T1, T2, T3)>
*
* And so on, with no need for T1, T2, ... Tn to be related types.
*/
invariant($post_data instanceof PostData, "This is good");
invariant(is_int($comment_count), "This is good");
return tuple($post_data, $comment_count);
};
// Transform the array of post IDs into an array of results,
// using the vm() function from asio-utilities
return await \HH\Asio\vm($all_post_ids, $post_fetcher);
}
async function generate_page(int $author_id): Awaitable<string> {
$tuples = await fetch_page_data($author_id);
$page = "";
foreach ($tuples as $tuple) {
list($post_data, $comment_count) = $tuple;
// Normally render the data into HTML, but for now, just create a
// normal string
$page .= $post_data->text . " " . $comment_count . PHP_EOL;
}
return $page;
}
$page = \HH\Asio\join(generate_page(13324)); // just made up a user id
var_dump($page);
Output
string(89) "AGEDMJQTFIVSCPHKLURWXNOZBY 9
ALSJURTKYIFBQMHXPNVWCDGZOE 25
GFMEYPITXDBORLVCKNAWJSUZQH 10
"
上面的例子遵循我们的流程:
- 每个提取操作的一个功能(ids,post text,comment count)。
- 数据操作的一个功能(文本和注释计数)。
- 一个协调一切的顶级功能。
考虑批处理
等待手柄可以重新安排。这意味着它将被发回到调度程序的队列,等待直到其他等待运行。批处理可以很好地利用重新安排。例如,假设您有高延迟查询数据,但您可以在单个请求中发送多个密钥进行查找。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\Batching;
// For asio-utilities function later(), etc.
async function b_one(string $key): Awaitable<string> {
$subkey = await Batcher::lookup($key);
return await Batcher::lookup($subkey);
}
async function b_two(string $key): Awaitable<string> {
return await Batcher::lookup($key);
}
async function batching(): Awaitable<void> {
$results = await \HH\Asio\v(array(b_one('hello'), b_two('world')));
echo $results[0] . PHP_EOL;
echo $results[1];
}
\HH\Asio\join(batching());
class Batcher {
private static array<string> $pendingKeys = array();
private static ?Awaitable<array<string, string>> $aw = null;
public static async function lookup(string $key): Awaitable<string> {
// Add this key to the pending batch
self::$pendingKeys[] = $key;
// If there's no awaitable about to start, create a new one
if (self::$aw === null) {
self::$aw = self::go();
}
// Wait for the batch to complete, and get our result from it
$results = await self::$aw;
return $results[$key];
}
private static async function go(): Awaitable<array<string, string>> {
// Let other awaitables get into this batch
await \HH\Asio\later();
// Now this batch has started; clear the shared state
$keys = self::$pendingKeys;
self::$pendingKeys = array();
self::$aw = null;
// Do the multi-key roundtrip
return await multi_key_lookup($keys);
}
}
async function multi_key_lookup(array<string> $keys)
: Awaitable<array<string, string>> {
// lookup multiple keys, but, for now, return something random
$r = array();
foreach ($keys as $key) {
$r[$key] = str_shuffle("ABCDEF");
}
return $r;
}
Output
/data/users/joelm/fbsource-opt/fbcode/_bin/hphp/hhvm/hhvm
BEACFD
FDCEBA
在上面的例子中,我们将包含数据信息的服务器的往返次数减少到两个,通过批处理第一个查找b_one()和查找b_two()。该Batcher::lookup()功能有助于实现这一减少。
将await HH\Asio\later()在Batcher::go()基本上允许Batcher::go()推迟到其他未决awaitables已经运行。
所以,await HH\Asio\v(array(b_one..., b_two...));有两个待决的等待。如果b_one()被称为第一个,它调用Batcher::lookup(),哪个调用Batcher::go(),哪些重新调度通过later()。然后HHVM寻找其他待处理的等待。b_two()也正在等待。它调用Batcher::lookup(),然后它就会通过暂停await self::$aw,因为Batcher::$aw不是null任何更长的时间。现在Batcher::go()恢复,获取并返回结果。
不要忘记Await an Awaitable
你觉得在这里发生什么?
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\ForgetAwait;
async function speak(): Awaitable<void> {
echo "one";
await \HH\Asio\later();
echo "two";
echo "three";
}
async function forget_await(): Awaitable<void> {
$handle = speak(); // This just gets you the handle
}
forget_await();
Output
one
答案是未定义的。你可能会得到所有三个回音。你可能只得到第一个回音。你根本不会得到任何东西。保证speak()
完成的唯一方法就是完成await
。await
是Async调度程序的触发器,允许HHVM适当地暂停和恢复speak()
; 否则,Async调度程序将不提供相关的保证speak()
。
最大限度地减少不必要的副作用
为了尽量减少任何不必要的副作用(例如排序差异),您的创建和等待等待应尽可能接近。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\SideEffects;
async function get_curl_data(string $url): Awaitable<string> {
return await \HH\Asio\curl_exec($url);
}
function possible_side_effects(): int {
sleep(1);
echo "Output buffer stuff";
return 4;
}
async function proximity(): Awaitable<void> {
$handle = get_curl_data("http://example.com");
possible_side_effects();
await $handle; // instead you should await get_curl_data("....") here
}
\HH\Asio\join(proximity());
Output
Output buffer stuff
在上述示例中,possible_side_effects()当您达到等待从网站获取数据相关的句柄时,可能会导致一些不期望的行为。
基本上,不依赖于同一代码的运行之间的输出顺序。即,不要编写Async代码,其中排序很重要,而是通过等待和使用依赖关系await。
备注可能会很好 但只有等待
由于Async通常用于耗时的操作,因此记录(即,缓存)Async调用的结果肯定是值得的。
该<<__Memoize>>属性做正确的事。所以,如果可以,使用它。但是,如果你需要的记忆化的明确的控制,确保你memoize的的awaitable,而不是等待它的结果。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\MemoizeResult;
async function time_consuming(): Awaitable<string> {
sleep(5);
return "This really is not time consuming, but the sleep fakes it.";
}
async function memoize_result(): Awaitable<string> {
static $result = null;
if ($result === null) {
$result = await time_consuming(); // don't memoize the resulting data
}
return $result;
}
function runMe(): void {
$t1 = microtime();
\HH\Asio\join(memoize_result());
$t2 = microtime() - $t1;
$t3 = microtime();
\HH\Asio\join(memoize_result());
$t4 = microtime() - $t3;
var_dump($t4 < $t2); // The memmoized result will get here a lot faster
}
runMe();
Output
bool(true)
表面看来,这似乎是合理的。我们要缓存与等待的相关的实际数据。然而,这可能会导致不良的竞争条件。
试想一下,还有另外两个Async函数等待的结果memoize_result(),称他们A()和B()。可能发生以下事件序列:
- A()得到运行,awaits memoize_result()。
- memoize_result()发现memoization缓存是空的($result是 null),所以它await是time_consuming()。它被暂停。
- B()得到运行,awaits memoize_result()。请注意,这是一个新的等待; 它不一样等于1。
- memoize_result()再次发现memoization缓存是空的,所以它等待time_consuming()再次。现在耗时的工作将会进行两次。
如果time_consuming()有副作用(例如数据库写),那么这可能是一个严重的错误。即使没有副作用,它仍然是一个bug; 耗时的操作正在进行多次,只需要完成一次。
相反,记住awaitable:
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\MemoizeAwaitable;
async function time_consuming(): Awaitable<string> {
sleep(5);
return "Not really time consuming but sleep."; // For type-checking purposes
}
function memoize_handle(): Awaitable<string> {
static $handle = null;
if ($handle === null) {
$handle = time_consuming(); // memoize the awaitable
}
return $handle;
}
function runMe(): void {
$t1 = microtime();
\HH\Asio\join(memoize_handle());
$t2 = microtime() - $t1;
$t3 = microtime();
\HH\Asio\join(memoize_handle());
$t4 = microtime() - $t3;
var_dump($t4 < $t2); // The memmoized result will get here a lot faster
}
runMe();
Output
bool(true)
这简单地缓存句柄并逐字返回 - Async Vs Awaitable可以更详细地解释这一点。
如果它是缓存后等待句柄的Async函数,这也将起作用。这可能看起来不直观,因为await每次执行该功能时,即使在缓存命中路径上也是如此。但是没关系,除了第一个执行之外的每个执行$handle都不行null,所以一个新的实例time_consuming()不会被启动。一个现有实例的结果将被共享。
任何一种方法都有效,但非Async缓存包装可以更容易理解。
尽可能地使用Lambdas
Lambdas可以减少编写完整关闭语法的代码冗长度。它们与Async实用工具协同工作非常有用。
例如,可以使用lambdas来缩短以下三种方式来完成相同的事情。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\Lambdas;
// For asio-utilities that we installed via composer
async function fourth_root(num $n): Awaitable<float> {
return sqrt(sqrt($n));
}
async function normal_call(): Awaitable<Vector<float>> {
$nums = Vector {64, 81};
return await \HH\Asio\vm(
$nums,
fun('\Hack\UserDocumentation\Async\Guidelines\Examples\Lambdas\fourth_root')
);
}
async function closure_call(): Awaitable<Vector<float>> {
$nums = Vector {64, 81};
$froots = async function(num $n): Awaitable<float> {
return sqrt(sqrt($n));
};
return await \HH\Asio\vm($nums, $froots);
}
async function lambda_call(): Awaitable<Vector<float>> {
$nums = Vector {64, 81};
return await \HH\Asio\vm($nums, async $num ==> sqrt(sqrt($num)));
}
async function use_lambdas(): Awaitable<void> {
$nc = await normal_call();
$cc = await closure_call();
$lc = await lambda_call();
var_dump($nc);
var_dump($cc);
var_dump($lc);
}
\HH\Asio\join(use_lambdas());
Output
object(HH\Vector)#8 (2) {
[0]=>
float(2.8284271247462)
[1]=>
float(3)
}
object(HH\Vector)#16 (2) {
[0]=>
float(2.8284271247462)
[1]=>
float(3)
}
object(HH\Vector)#24 (2) {
[0]=>
float(2.8284271247462)
[1]=>
float(3)
}
在Non-async功能中使用join
想象一下,您正在从非同步范围调用async函数join_async()。为了获得您期望的结果,您必须join()为了获得等待的结果。
<?hh
namespace Hack\UserDocumentation\Async\Guidelines\Examples\Join;
async function join_async(): Awaitable<string> {
return "Hello";
}
// In an async function, you would await an awaitable.
// In a non-async function, or the global scope, you can
// use `join` to force the the awaitable to run to its completion.
$s = \HH\Asio\join(join_async());
var_dump($s);
Output
string(5) "Hello"
这种情况通常发生在全局范围内(但可能发生在任何地方)。
记住Async不是多线程
Async功能不在同一时间运行。它们是通过在执行代码中等待状态的改变(即抢占式多任务)来进行CPU共享。Async还存在于正常PHP和Hack的单线程世界中。
await 不是表达
你可以await在三个地方使用:
- 作为一个声明本身(例如,await func())
- 在任务的右侧(RHS)(例如$r = await func())
- 作为return(例如return await func())的论据
你不能,例如,用await在var_dump()。