Guidelines

2018-10-12 11:37 更新

這可能是誘人的,只要將async,awaitAwaitable放在你所有的代碼里。雖然可以有更多的async功能 - 事實(shí)上,你一般不應(yīng)該害怕做一個(gè)功能,async因?yàn)闆](méi)有性能損失這樣做 - 有一些準(zhǔn)則,你應(yīng)該遵循,以最大限度地發(fā)揮有效利用async。

Be Liberal, but Careful, with Async

如果你正在努力為您的代碼是否應(yīng)該是Async與否,通??梢蚤_(kāi)始尋找答案肯定,并找到一個(gè)理由說(shuō)沒(méi)有。例如,一個(gè)簡(jiǎn)單的hello world程序可以用Async處理,沒(méi)有性能損失。您可能無(wú)法獲得任何收益,但您不會(huì)收到任何損失 - 它將針對(duì)任何可能需要Async的更改進(jìn)行設(shè)置。

這兩個(gè)程序是為了所有意圖和目的,等同的。

<?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"

只要確保你遵循其余的準(zhǔn)則。Async非常好,但您仍然需要考慮緩存,批量和效率等方面。

使用Async擴(kuò)展

對(duì)于A(yíng)sync將提供最大效益的常見(jiàn)情況,HHVM提供方便的擴(kuò)展庫(kù),以幫助編寫(xiě)代碼更容易。根據(jù)您的用例情況,您應(yīng)該自由使用:

  • MySQL用于數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)和查詢(xún)。
  • cURL用于網(wǎng)頁(yè)數(shù)據(jù)和傳輸。
  • McRouter用于基于memcached的操作。
  • Streams的基于Streams的資源操作。

不要在循環(huán)中使用Async

如果您只記住一條規(guī)則,請(qǐng)記住:

**不要await循環(huán)**

它完全違反了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"

在上面的例子中,循環(huán)正在做兩件事情:

  1. 使循環(huán)迭代成為如何運(yùn)行代碼的限制因素。通過(guò)循環(huán),您可以確保順序獲取用戶(hù)。
  2. 您正在創(chuàng)建錯(cuò)誤的依賴(lài)關(guān)系。加載一個(gè)用戶(hù)不依賴(lài)于加載另一個(gè)用戶(hù)。

相反,您將需要使用我們的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"

考慮到數(shù)據(jù)依賴(lài)性很重要

學(xué)習(xí)如何構(gòu)建Async代碼最重要的方面是理解數(shù)據(jù)依賴(lài)關(guān)系模式。以下是如何確保Async代碼是數(shù)據(jù)依賴(lài)性的一般流程:

  1. 將每個(gè)沒(méi)有分支(鏈)的依賴(lài)關(guān)系序列放入其自己的async函數(shù)中。
  2. 將每條并行鏈捆綁到其自己的async功能中。
  3. 重復(fù)一下,看看是否進(jìn)一步減少。

假設(shè)我們正在收到作者的博客文章。這將涉及以下步驟:

  1. 獲取作者的帖子ID。
  2. 獲取每個(gè)帖子ID的帖子。
  3. 獲取每個(gè)帖子ID的評(píng)論數(shù)。
  4. 生成最后一頁(yè)信息
<?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. 每個(gè)提取操作的一個(gè)功能(ids,post text,comment count)。
  2. 數(shù)據(jù)操作的一個(gè)功能(文本和注釋計(jì)數(shù))。
  3. 一個(gè)協(xié)調(diào)一切的頂級(jí)功能。

考慮批處理

等待手柄可以重新安排。這意味著它將被發(fā)回到調(diào)度程序的隊(duì)列,等待直到其他等待運(yùn)行。批處理可以很好地利用重新安排。例如,假設(shè)您有高延遲查詢(xún)數(shù)據(jù),但您可以在單個(gè)請(qǐng)求中發(fā)送多個(gè)密鑰進(jìn)行查找。

<?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

在上面的例子中,我們將包含數(shù)據(jù)信息的服務(wù)器的往返次數(shù)減少到兩個(gè),通過(guò)批處理第一個(gè)查找b_one()和查找b_two()。該Batcher::lookup()功能有助于實(shí)現(xiàn)這一減少。

將await HH\Asio\later()在Batcher::go()基本上允許Batcher::go()推遲到其他未決awaitables已經(jīng)運(yùn)行。

所以,await HH\Asio\v(array(b_one..., b_two...));有兩個(gè)待決的等待。如果b_one()被稱(chēng)為第一個(gè),它調(diào)用Batcher::lookup(),哪個(gè)調(diào)用Batcher::go(),哪些重新調(diào)度通過(guò)later()。然后HHVM尋找其他待處理的等待。b_two()也正在等待。它調(diào)用Batcher::lookup(),然后它就會(huì)通過(guò)暫停await self::$aw,因?yàn)锽atcher::$aw不是null任何更長(zhǎng)的時(shí)間?,F(xiàn)在Batcher::go()恢復(fù),獲取并返回結(jié)果。

不要忘記Await an Awaitable

你覺(jué)得在這里發(fā)生什么?

<?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

答案是未定義的。你可能會(huì)得到所有三個(gè)回音。你可能只得到第一個(gè)回音。你根本不會(huì)得到任何東西。保證speak()完成的唯一方法就是完成await。await是Async調(diào)度程序的觸發(fā)器,允許HHVM適當(dāng)?shù)貢和:突謴?fù)speak(); 否則,Async調(diào)度程序?qū)⒉惶峁┫嚓P(guān)的保證speak()。

最大限度地減少不必要的副作用

為了盡量減少任何不必要的副作用(例如排序差異),您的創(chuàng)建和等待等待應(yīng)盡可能接近。

<?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()當(dāng)您達(dá)到等待從網(wǎng)站獲取數(shù)據(jù)相關(guān)的句柄時(shí),可能會(huì)導(dǎo)致一些不期望的行為。

基本上,不依賴(lài)于同一代碼的運(yùn)行之間的輸出順序。即,不要編寫(xiě)Async代碼,其中排序很重要,而是通過(guò)等待和使用依賴(lài)關(guān)系await。

備注可能會(huì)很好 但只有等待

由于A(yíng)sync通常用于耗時(shí)的操作,因此記錄(即,緩存)Async調(diào)用的結(jié)果肯定是值得的。

<<__Memoize>>屬性做正確的事。所以,如果可以,使用它。但是,如果你需要的記憶化的明確的控制,確保你memoize的的awaitable,而不是等待它的結(jié)果。

<?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)

表面看來(lái),這似乎是合理的。我們要緩存與等待的相關(guān)的實(shí)際數(shù)據(jù)。然而,這可能會(huì)導(dǎo)致不良的競(jìng)爭(zhēng)條件。

試想一下,還有另外兩個(gè)Async函數(shù)等待的結(jié)果memoize_result(),稱(chēng)他們A()和B()。可能發(fā)生以下事件序列:

  1. A()得到運(yùn)行,awaits memoize_result()。
  2. memoize_result()發(fā)現(xiàn)memoization緩存是空的($result是 null),所以它await是time_consuming()。它被暫停。
  3. B()得到運(yùn)行,awaits memoize_result()。請(qǐng)注意,這是一個(gè)新的等待; 它不一樣等于1。
  4. memoize_result()再次發(fā)現(xiàn)memoization緩存是空的,所以它等待time_consuming()再次?,F(xiàn)在耗時(shí)的工作將會(huì)進(jìn)行兩次。

如果time_consuming()有副作用(例如數(shù)據(jù)庫(kù)寫(xiě)),那么這可能是一個(gè)嚴(yán)重的錯(cuò)誤。即使沒(méi)有副作用,它仍然是一個(gè)bug; 耗時(shí)的操作正在進(jìn)行多次,只需要完成一次。

相反,記住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)

這簡(jiǎn)單地緩存句柄并逐字返回 - Async Vs Awaitable可以更詳細(xì)地解釋這一點(diǎn)。

如果它是緩存后等待句柄的Async函數(shù),這也將起作用。這可能看起來(lái)不直觀(guān),因?yàn)閍wait每次執(zhí)行該功能時(shí),即使在緩存命中路徑上也是如此。但是沒(méi)關(guān)系,除了第一個(gè)執(zhí)行之外的每個(gè)執(zhí)行$handle都不行null,所以一個(gè)新的實(shí)例time_consuming()不會(huì)被啟動(dòng)。一個(gè)現(xiàn)有實(shí)例的結(jié)果將被共享。

任何一種方法都有效,但非Async緩存包裝可以更容易理解。

盡可能地使用Lambdas

Lambdas可以減少編寫(xiě)完整關(guān)閉語(yǔ)法的代碼冗長(zhǎng)度。它們與Async實(shí)用工具協(xié)同工作非常有用。

例如,可以使用lambdas來(lái)縮短以下三種方式來(lái)完成相同的事情。

<?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

想象一下,您正在從非同步范圍調(diào)用async函數(shù)join_async()。為了獲得您期望的結(jié)果,您必須join()為了獲得等待的結(jié)果。

<?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"

這種情況通常發(fā)生在全局范圍內(nèi)(但可能發(fā)生在任何地方)。

記住Async不是多線(xiàn)程

Async功能不在同一時(shí)間運(yùn)行。它們是通過(guò)在執(zhí)行代碼中等待狀態(tài)的改變(即搶占式多任務(wù))來(lái)進(jìn)行CPU共享。Async還存在于正常PHP和Hack的單線(xiàn)程世界中。

await 不是表達(dá)

你可以await在三個(gè)地方使用:

  1. 作為一個(gè)聲明本身(例如,await func())
  2. 在任務(wù)的右側(cè)(RHS)(例如$r = await func())
  3. 作為return(例如return await func())的論據(jù)

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


以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)