Guidelines

2018-10-12 11:37 更新

这可能是诱人的,只要将asyncawaitAwaitable放在你所有的代码里。虽然可以有更多的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提供方便的扩展库,以帮助编写代码更容易。根据您的用例情况,您应该自由使用:

  • MySQL用于数据库访问和查询。
  • cURL用于网页数据和传输。
  • McRouter用于基于memcached的操作。
  • Streams的基于Streams的资源操作。

不要在循环中使用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"

在上面的例子中,循环正在做两件事情:

  1. 使循环迭代成为如何运行代码的限制因素。通过循环,您可以确保顺序获取用户。
  2. 您正在创建错误的依赖关系。加载一个用户不依赖于加载另一个用户。

相反,您将需要使用我们的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代码是数据依赖性的一般流程:

  1. 将每个没有分支(链)的依赖关系序列放入其自己的async函数中。
  2. 将每条并行链捆绑到其自己的async功能中。
  3. 重复一下,看看是否进一步减少。

假设我们正在收到作者的博客文章。这将涉及以下步骤:

  1. 获取作者的帖子ID。
  2. 获取每个帖子ID的帖子。
  3. 获取每个帖子ID的评论数。
  4. 生成最后一页信息
<?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
"

上面的例子遵循我们的流程:

  1. 每个提取操作的一个功能(ids,post text,comment count)。
  2. 数据操作的一个功能(文本和注释计数)。
  3. 一个协调一切的顶级功能。

考虑批处理

等待手柄可以重新安排。这意味着它将被发回到调度程序的队列,等待直到其他等待运行。批处理可以很好地利用重新安排。例如,假设您有高延迟查询数据,但您可以在单个请求中发送多个密钥进行查找。

<?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()完成的唯一方法就是完成awaitawait是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()。可能发生以下事件序列:

  1. A()得到运行,awaits memoize_result()。
  2. memoize_result()发现memoization缓存是空的($result是 null),所以它await是time_consuming()。它被暂停。
  3. B()得到运行,awaits memoize_result()。请注意,这是一个新的等待; 它不一样等于1。
  4. 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在三个地方使用:

  1. 作为一个声明本身(例如,await func())
  2. 在任务的右侧(RHS)(例如$r = await func())
  3. 作为return(例如return await func())的论据

你不能,例如,用await在var_dump()


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号