Async/await

出典: フリー百科事典『ウィキペディア(Wikipedia)』

async/awaitパターンは、多くのプログラミング言語における構文機能であり、非同期非ブロッキング関数を通常の同期関数と同様の方法で構築できる。それは意味的にコルーチンの概念と関連し、多くの場合は類似した技術を使用して実装される。主に実行時間の長い非同期タスクの完了を待っている間に他のコードを実行する機会の提供を目的とし、通常は promise または同様のデータ構造で表される。

この機能はC# 5.0、VB.NET 11、Python 3.5、HackDartKotlin 1.1、Rust 1.39[1]Nim 0.9.4[2]ECMAScript (JavaScript) 2017 (ES2017)、C++20にて利用できるほか、Scala[3]などでもいくつかの拡張、ベータ版、および特定の実装において実験的な成果物がある。

例:C#[編集]

以下のC#メソッド関数)は、指定されたURIからリソースをダウンロードし、そのリソースの長さを返す。

public static async Task<int> FindPageSize(Uri uri) 
{
    byte[] data = await new WebClient().DownloadDataTaskAsync(uri);
    return data.Length;
}
  • まず、asyncキーワードはC#コンパイラーに対してメソッドが非同期であることを示す。つまり、任意の数のawait式を使用でき、結果をpromiseにバインドする。
  • 戻り値の型であるTask<T> は、C#におけるpromiseの類似形であり、ここではint型の結果値を持つことが示されている。
  • このメソッドが呼び出されたときに最初に実行される式は、new WebClient() である。DownloadDataTaskAsync(uri)は、Task<byte[]>を返す別の非同期メソッドである[4]。このメソッドは非同期であるため、戻る前にデータのバッチ全体をダウンロードしない。代わりに、非ブロッキングメカニズム(ハードウェアによりオフロードされた実行コンテキストや、バックグラウンドスレッドなど)を使用してダウンロードプロセスを開始し、解決も拒否もされていないTask<byte[]>をこのメソッドに対して即座に返す。
  • Taskにアタッチされたawaitキーワードによって、このメソッドはすぐにTask<int>を呼び出し元に返し、呼び出し元は必要に応じて他の処理を続行できる。
  • DownloadDataTaskAsync()がダウンロードを完了すると、そのダウンロードしたデータによって、以前返却していたTaskを解決する。これによりコールバックがトリガーされ、その値をdataに割り当てることでFindPageSize()に実行を継続させる。
  • 最後にメソッドは、配列の長さを示す単純な整数値であるdata.Lengthを返す。コンパイラーは、以前に返却されたTaskを解決するものとしてこれを再解釈し、その長さの値を使って何かをするためにメソッドの呼び出し元においてコールバックをトリガーする。

async/awaitを使用するメソッドは、必要な数のawait式を使用でき、それぞれが同じ方法で処理される(promiseは最初のawaitに対してのみ呼び出し元に返されるが、他のすべてのawaitは内部コールバックを利用する)。関数はpromiseオブジェクトを直接保持し、最初に他の処理(他の非同期タスクの開始を含む)を実行して、結果が必要になるまでpromiseの待機を遅らせることもできる。promiseを使用する関数には、複数のpromiseを一度にまたは特定のパターン(C#のTaskなど)で待機できるようにするpromise集計メソッドもある: Task.WhenAll() は、引数内のすべてのTaskが解決されたときに解決される、値のないTaskを返す。多くのpromiseタイプには、複数の結果コールバックを設定したり、特に長時間実行されるタスクの進行状況を検査したりできるなど、async/awaitパターンが通常使用する機能を超える追加機能がある。

C#の特定のケース、およびこの言語機能を備えた他の多くの言語では、async/awaitパターンは言語のランタイムのコアパーツではなく、コンパイル時にラムダまたは継続を使用して実装される。たとえば、C#コンパイラーは、上記のコードをIL バイトコード形式に変換する前に、次のようなコードに変換する可能性がある。

public static Task<int> FindPageSize(Uri uri) 
{
    Task<byte[]> data_task = new WebClient().DownloadDataTaskAsync(uri);
    Task<int> after_data_task = data_task.ContinueWith((original_task) => {
        return original_task.Result.Length;
    });
    return after_data_task;
}

このため、インターフェイスメソッドがpromiseオブジェクトを返す必要があるが、それ自体が非同期タスクを待機するawaitに本文での待機を必要としない場合、async修飾子も必要なく、代わりにpromiseオブジェクトを直接返すことができる。たとえば、関数はいくつかの結果値(C#のTask.FromResult()など)にすぐに解決されるプロミスを提供できる場合があり、または単純に、必要な正確なプロミスである別のメソッドのプロミスを返す場合もある(オーバーロードによって遅延する場合など)。

しかし、この機能の一つの重要な注意点は、コードは従来のブロッキングコードに似ている一方で、コードが実際に非ブロックおよびマルチスレッドであることにより、awaitの付いた目標のプロミスが解決するのを待っている間に、多くの介在事象が発生する可能性があることを意味する。たとえば、次のコードは、awaitなしで常にブロッキングモデルで成功するが、await中にイベントが発生する可能性があるため、その下から共有状態が変更されていることがわかる。

var a = state.a;
var data = await new WebClient().DownloadDataTaskAsync(uri);
Debug.Assert(a == state.a); // イベントハンドラーの介入により値が変更される潜在的な問題がある。
return data.Length;

F#の場合[編集]

2007年のF#リリースには、「非同期ワークフロー」が含まれている[5] 。非同期ワークフローはCE(コンピュテーション式)として記述され、(C#のasyncのような)特別なコンテキストを指定することなく定義できる。非同期ワークフローを開始するには、キーワードに感嘆符(!)を付加する。

次の非同期関数では、非同期ワークフローを使用してURLで示すデータをダウンロードする。

let asyncSumPageSizes (uris: #seq<Uri>) : Async<int> = async {
    use httpClient = new HttpClient()
    let! pages = 
        uris
        |> Seq.map(httpClient.GetStringAsync >> Async.AwaitTask)
        |> Async.Parallel
    return pages |> Seq.fold (fun accumulator current -> current.Length + accumulator) 0
}

C#の場合[編集]

2011年にリリースされたAsync CTPでプロトタイプ版が実装され[6]、2012年のC# 5.0で正式にサポートされた。

C# 7より前のバージョンでは、非同期メソッドはvoidTask、またはTask<T>を返すことが要求される。これはC# 7で拡張され、ValueTask<T>などの他の特定の型が含まれるようになった。voidを返す非同期メソッドは、イベントハンドラーを対象としている。同期メソッドがvoidを返すような大抵のケースでは、より直感的な例外処理を可能にするため、代わりにTaskを返すことが推奨される[7]

awaitを使用するメソッドは、asyncキーワードを付けて宣言する必要がある。Task<T>型の戻り値を持つメソッドでは、async宣言されたメソッドには、Task<T>ではなくTに割り当て可能な型のreturn文が必要である。コンパイラは値をTask<T>ジェネリックでラップする。asyncなしで宣言されたTaskまたはTask<T>戻り値の型を持つメソッドをawaitすることもできる。

次の非同期メソッドは、awaitを使用してURLからデータをダウンロードする。

public async Task<int> SumPageSizesAsync(IList<Uri> uris) 
{
    int total = 0;
    foreach (var uri in uris)
    {
        this.statusText.Text = string.Format("Found {0} bytes ...", total);
        var data = await new WebClient().DownloadDataTaskAsync(uri);
        total += data.Length;
    }
    this.statusText.Text = string.Format("Found {0} bytes total", total);
    return total;
}

注意: C#コンパイラのawait対応正式版リリース時から、出力されるコードはステートマシンによる実行効率性の高いコードを生成するように実装されている。そのため、冒頭で示した、promiseをTask.ContinueWith(...)によって継続させる疑似コードの議論は、現実のC#コンパイラには当てはまらない。また、ステートマシンは、コンパイラに完全にハードコーディングされているわけではなく、AsyncStateMachineAttribute属性を使用して、ライブラリ提供者が独自のステートマシンを提供できる拡張ポイントを有している。ValueTaskなどの、より軽量なpromiseと対応するステートマシンは、この機能を使用して実装されている。

Scalaの場合[編集]

Scalaの実験的なScala-async拡張機能では、通常のメソッドとは異なるものの、awaitは「メソッド」である。さらに、メソッドを非同期としてマークする必要があるC# 5.0とは異なり、Scala-asyncでは、コードの「ブロック」は非同期の「呼び出し」で囲まれる。

使い方[編集]

Scala-asyncでは、asyncは実際には、Scalaマクロを使用して実装される。これにより、コンパイラは異なるコードを発行し、有限状態マシン実装を生成する(モナディック実装より効率的であると考えられるが、手で書くのに不便ではある)。

Scala-asyncが非同期のものを含む、異なるさまざまな実装をサポートする計画もある。

Pythonの場合[編集]

Python 3.5は、async/awaitのサポートを追加した。PEP0492( https://www.python.org/dev/peps/pep-0492/ )を参照のこと。

import asyncio

async def main():
    print("hello")
    await asyncio.sleep(1)
    print("world")

asyncio.run(main())

JavaScriptの場合[編集]

JavaScriptのawait演算子は、非同期関数内からのみ使用できる。パラメータがpromiseの場合、promiseが解決されると非同期関数の実行が再開される(promiseが拒否されない限り。拒否された場合、通常のJavaScript 例外処理で処理できるエラーがスローされる)。パラメータがpromiseでない場合、パラメータ自体はすぐに返される[8]

多くのライブラリは、ネイティブJavaScriptプロミスの仕様に一致する限り、awaitでも使用できるpromiseオブジェクトを提供する。ただし、jQueryライブラリのpromiseは、jQuery 3.0まではPromises/A+互換ではなかった[9]

次に例を示す(この記事[10]から一部改変):

async function createNewDoc() {
   const response = await db.post({}); // docを送信する
   return await db.get(response.id); // idで検索する
}

async function main() {
   try {
      const doc = await createNewDoc();
      console.log(doc);
   } catch (err) {
      console.log(err);
   }
}
main();

Node.jsバージョン8には、標準ライブラリのコールバックベースのメソッドをPromiseとして使用できるようにするユーティリティが含まれている[11]

C++の場合[編集]

C++では、awaitが正式にC++20ドラフトにマージされたため、正式なC++20の一部として正式に受理される予定である[12]。ただし、実際のC++キーワードはawaitではなくco_awaitという名前になった。また、MSVCコンパイラとClangコンパイラは、少なくとも何らかの形式のco_awaitをすでにサポートしている(GCCはまだサポートしていない)。

#include <future>
#include <iostream>

using namespace std;

future<int> add(int a, int b)
{
    int c = a + b;
    co_return c;
}

future<void> test()
{
    int ret = co_await add(1, 2);
    cout << "return " << ret << endl;
}

int main()
{
    auto fut = test();
    fut.wait();

    return 0;
}

Cの場合[編集]

C言語でのawait/asyncの正式なサポートはまだ存在しない。s_taskなどの一部のコルーチンライブラリは、マクロでawait/asyncキーワードをシミュレートする。

#include <stdio.h>
#include "s_task.h"

// タスク用にメモリ定義
int g_stack_main[64 * 1024 / sizeof(int)];
int g_stack0[64 * 1024 / sizeof(int)];
int g_stack1[64 * 1024 / sizeof(int)];

void sub_task(__async__, void* arg) {
    int i;
    int n = (int)(size_t)arg;
    for (i = 0; i < 5; ++i) {
        printf("task %d, delay seconds = %d, i = %d\n", n, n, i);
        s_task_msleep(__await__, n * 1000);
        //s_task_yield(__await__);
    }
}

void main_task(__async__, void* arg) {
    int i;

    // 2つのサブタスクを作成
    s_task_create(g_stack0, sizeof(g_stack0), sub_task, (void*)1);
    s_task_create(g_stack1, sizeof(g_stack1), sub_task, (void*)2);

    for (i = 0; i < 4; ++i) {
        printf("task_main arg = %p, i = %d\n", arg, i);
        s_task_yield(__await__);
    }

    // サブタスクの終了を待つ
    s_task_join(__await__, g_stack0);
    s_task_join(__await__, g_stack1);
}

int main(int argc, char* argv) {

    s_task_init_system();

    // メインタスクを作成
    s_task_create(g_stack_main, sizeof(g_stack_main), main_task, (void*)(size_t)argc);
    s_task_join(__await__, g_stack_main);
    printf("all task is over\n");
    return 0;
}

Perl5の場合[編集]

Future::AsyncAwaitモジュールは、2018年9月のPerl財団助成金の対象であった[13]

Rustの場合[編集]

2019年11月7日、async/awaitがRustの安定バージョンで利用可能になった[14]。Rustにおいて、非同期関数はFutureトレイトを実装する値を返すプレーンな関数に脱糖(desugar)される。現在は、それらは有限状態マシンで実装される。

// futuresクレートを使用するために、クレートのCargo.tomlの依存関係セクションに`futures = "0.3.0"`を定義する必要あり。

extern crate futures; // 現在、`std`ライブラリに executor は存在しない。

// 以下のように脱糖(desugar)される。
//   `fn async_add_one(num: u32) -> impl Future<Output = u32>`
async fn async_add_one(num: u32) -> u32 {
    num + 1
}

async fn example_task() {
    let number = async_add_one(5).await;
    println!("5 + 1 = {}", number);
}

fn main() {
    // futureは、作成された時点ではタスクは開始されない。
    let future = example_task(5);

    // JavaScriptと異なり、futureはポーリングされて初めて開始される。
    futures::executor::block_on(future);
}

メリットと批判[編集]

Async/awaitパターンをサポートする言語の大きな利点は、非同期の非ブロッキングコードを最小限のオーバーヘッドで記述でき、従来の同期ブロックコードとほとんど同じように見えることである。特にawaitは、メッセージパッシングプログラムで非同期コードを記述する最良の方法であると主張されてきた。特に、ブロッキングコードに近いため、読みやすさと定型コードの最小量が利点として挙げられた[15] 。その結果、async/awaitを使用すると、ほとんどのプログラマーがプログラムについて推論しやすくなり、awaitは、それを必要とするアプリケーションでより優れた、より堅牢なノンブロッキングコードを促進する傾向がある。このようなアプリケーションは、グラフィカルユーザインタフェースを提供するプログラムから、ゲームや金融アプリケーションなど、非常にスケーラブルなステートフルなサーバー側プログラムまでさまざまである。

awaitが批判されるときには、awaitは周囲のコードも非同期になる傾向があることがしばしば指摘される。一方で、このコードの伝染性(「ゾンビウイルス」と比較されることもある)はあらゆる種類の非同期プログラミングに固有であると主張されてきたため、この点に関してはawaitだけに特有のものではない[7]

関連項目[編集]

脚注[編集]

  1. ^ Announcing Rust 1.39.0” (英語). 2019年11月7日閲覧。
  2. ^ Version 0.9.4 released - Nim blog” (英語). 2020年1月19日閲覧。
  3. ^ Scala Async”. 2013年10月20日閲覧。
  4. ^ .NETにおいて、Taskを返すメソッドの名前は、慣例的にAsyncの接尾辞が付けられる。
  5. ^ Introducing F# Asynchronous Workflows”. 2020年6月5日閲覧。
  6. ^ Asynchrony in C# 5, Part One”. 2021年4月18日閲覧。
  7. ^ a b Stephen Cleary, Async/Await - Best Practices in Asynchronous Programming
  8. ^ await - JavaScript (MDN)”. 2017年5月2日閲覧。
  9. ^ jQuery Core 3.0 Upgrade Guide”. 2017年5月2日閲覧。
  10. ^ Taming the asynchronous beast with ES7”. 2015年11月12日閲覧。
  11. ^ Foundation. “Node v8.0.0 (Current) - Node.js”. Node.js. 2020年6月5日閲覧。
  12. ^ ISO C++ Committee announces that C++20 design is now feature complete”. 2020年6月5日閲覧。
  13. ^ September 2018 Grant Votes - The Perl Foundation”. news.perlfoundation.org. 2019年3月26日閲覧。
  14. ^ Matsakis. “Async-await on stable Rust!”. Rust Blog. 2019年11月7日閲覧。
  15. ^ 'No Bugs' Hare. Eight ways to handle non-blocking returns in message-passing programs CPPCON, 2018