原文と比べた結果、この記事には多数の(または内容の大部分に影響ある)誤訳があることが判明しています。情報の利用には注意してください。正確な表現に改訳できる方を求めています。(2021年2月) |
async/awaitパターンは、多くのプログラミング言語における構文機能であり、非同期非ブロッキング関数を通常の同期関数と同様の方法で構築できる。それは意味的にコルーチンの概念と関連し、多くの場合は類似した技術を使用して実装される。主に実行時間の長い非同期タスクの完了を待っている間に他のコードを実行する機会の提供を目的とし、通常はpromise または同様のデータ構造で表される。
この機能はC# 5.0、VB.NET 11、Python 3.5、Hack、Dart、Kotlin 1.1、Rust 1.39[1]、Nim 0.9.4[2]、ECMAScript (JavaScript) 2017 (ES2017)、C++20にて利用できるほか、Scala[3]などでもいくつかの拡張、ベータ版、および特定の実装において実験的な成果物がある。
以下のC#メソッド(関数)は、指定されたURIからリソースをダウンロードし、そのリソースの長さを返す。
publicstaticasyncTask<int>FindPageSize(Uriuri){byte[]data=awaitnewWebClient().DownloadDataTaskAsync(uri);returndata.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バイトコード形式に変換する前に、次のようなコードに変換する可能性がある。
publicstaticTask<int>FindPageSize(Uriuri){Task<byte[]>data_task=newWebClient().DownloadDataTaskAsync(uri);Task<int>after_data_task=data_task.ContinueWith((original_task)=>{returnoriginal_task.Result.Length;});returnafter_data_task;}
このため、インターフェイスメソッドがpromiseオブジェクトを返す必要があるが、それ自体が非同期タスクを待機するawaitに本文での待機を必要としない場合、async修飾子も必要なく、代わりにpromiseオブジェクトを直接返すことができる。たとえば、関数はいくつかの結果値(C#のTask.FromResult()など)にすぐに解決されるプロミスを提供できる場合があり、または単純に、必要な正確なプロミスである別のメソッドのプロミスを返す場合もある(オーバーロードによって遅延する場合など)。
しかし、この機能の一つの重要な注意点は、コードは従来のブロッキングコードに似ている一方で、コードが実際に非ブロックおよびマルチスレッドであることにより、awaitの付いた目標のプロミスが解決するのを待っている間に、多くの介在事象が発生する可能性があることを意味する。たとえば、次のコードは、awaitなしで常にブロッキングモデルで成功するが、await中にイベントが発生する可能性があるため、その下から共有状態が変更されていることがわかる。
vara=state.a;vardata=awaitnewWebClient().DownloadDataTaskAsync(uri);Debug.Assert(a==state.a);// イベントハンドラーの介入により値が変更される潜在的な問題がある。returndata.Length;
2007年のF#リリースには、「非同期ワークフロー」が含まれている[5] 。非同期ワークフローはCE(コンピュテーション式)として記述され、(C#のasyncのような)特別なコンテキストを指定することなく定義できる。非同期ワークフローを開始するには、キーワードに感嘆符(!)を付加する。
次の非同期関数では、非同期ワークフローを使用してURLで示すデータをダウンロードする。
letasyncSumPageSizes(uris:#seq<Uri>):Async<int>=async{usehttpClient=newHttpClient()let!pages=uris|>Seq.map(httpClient.GetStringAsync>>Async.AwaitTask)|>Async.Parallelreturnpages|>Seq.fold(funaccumulatorcurrent->current.Length+accumulator)0}
2011年にリリースされたAsyncCTPでプロトタイプ版が実装され[6]、2012年のC# 5.0で正式にサポートされた。
C# 7より前のバージョンでは、非同期メソッドはvoid、Task、または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からデータをダウンロードする。
publicasyncTask<int>SumPageSizesAsync(IList<Uri>uris){inttotal=0;foreach(varuriinuris){this.statusText.Text=string.Format("Found {0} bytes ...",total);vardata=awaitnewWebClient().DownloadDataTaskAsync(uri);total+=data.Length;}this.statusText.Text=string.Format("Found {0} bytes total",total);returntotal;}
注意: C#コンパイラのawait対応正式版リリース時から、出力されるコードはステートマシンによる実行効率性の高いコードを生成するように実装されている。そのため、冒頭で示した、promiseをTask.ContinueWith(...)によって継続させる疑似コードの議論は、現実のC#コンパイラには当てはまらない。また、ステートマシンは、コンパイラに完全にハードコーディングされているわけではなく、AsyncStateMachineAttribute属性を使用して、ライブラリ提供者が独自のステートマシンを提供できる拡張ポイントを有している。ValueTaskなどの、より軽量なpromiseと対応するステートマシンは、この機能を使用して実装されている。
Scalaの実験的なScala-async拡張機能では、通常のメソッドとは異なるものの、awaitは「メソッド」である。さらに、メソッドを非同期としてマークする必要があるC# 5.0とは異なり、Scala-asyncでは、コードの「ブロック」は非同期の「呼び出し」で囲まれる。
Scala-asyncでは、asyncは実際には、Scalaマクロを使用して実装される。これにより、コンパイラは異なるコードを発行し、有限状態マシン実装を生成する(モナディック実装より効率的であると考えられるが、手で書くのに不便ではある)。
Scala-asyncが非同期のものを含む、異なるさまざまな実装をサポートする計画もある。
Python 3.5は、async/awaitのサポートを追加した。PEP0492(https://www.python.org/dev/peps/pep-0492/ )を参照のこと。
importasyncioasyncdefmain():print("hello")awaitasyncio.sleep(1)print("world")asyncio.run(main())
JavaScriptのawait演算子は、非同期関数内からのみ使用できる。パラメータがpromiseの場合、promiseが解決されると非同期関数の実行が再開される(promiseが拒否されない限り。拒否された場合、通常のJavaScript例外処理で処理できるエラーがスローされる)。パラメータがpromiseでない場合、パラメータ自体はすぐに返される[8]。
多くのライブラリは、ネイティブJavaScriptプロミスの仕様に一致する限り、awaitでも使用できるpromiseオブジェクトを提供する。ただし、jQueryライブラリのpromiseは、jQuery 3.0まではPromises/A+互換ではなかった[9]。
次に例を示す(この記事[10]から一部改変):
asyncfunctioncreateNewDoc(){constresponse=awaitdb.post({});// docを送信するreturnawaitdb.get(response.id);// idで検索する}asyncfunctionmain(){try{constdoc=awaitcreateNewDoc();console.log(doc);}catch(err){console.log(err);}}main();
Node.jsバージョン8には、標準ライブラリのコールバックベースのメソッドをPromiseとして使用できるようにするユーティリティが含まれている[11]。
C++では、awaitが正式にC++20ドラフトにマージされたため、正式なC++20の一部として正式に受理される予定である[12]。ただし、実際のC++キーワードはawaitではなくco_awaitという名前になった。また、MSVCコンパイラとClangコンパイラは、少なくとも何らかの形式のco_awaitをすでにサポートしている(GCCはまだサポートしていない)。
#include<future>#include<iostream>usingnamespacestd;future<int>add(inta,intb){intc=a+b;co_returnc;}future<void>test(){intret=co_awaitadd(1,2);cout<<"return "<<ret<<endl;}intmain(){autofut=test();fut.wait();return0;}
C言語でのawait/asyncの正式なサポートはまだ存在しない。s_taskなどの一部のコルーチンライブラリは、マクロでawait/asyncキーワードをシミュレートする。
#include<stdio.h>#include"s_task.h"// タスク用にメモリ定義intg_stack_main[64*1024/sizeof(int)];intg_stack0[64*1024/sizeof(int)];intg_stack1[64*1024/sizeof(int)];voidsub_task(__async__,void*arg){inti;intn=(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__);}}voidmain_task(__async__,void*arg){inti;// 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);}intmain(intargc,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");return0;}
Future::AsyncAwaitモジュールは、2018年9月のPerl財団助成金の対象であった[13]。
2019年11月7日、async/awaitがRustの安定バージョンで利用可能になった[14]。Rustにおいて、非同期関数はFutureトレイトを実装する値を返すプレーンな関数に脱糖(desugar)される。現在は、それらは有限状態マシンで実装される。
// futuresクレートを使用するために、クレートのCargo.tomlの依存関係セクションに`futures = "0.3.0"`を定義する必要あり。externcratefutures;// 現在、`std`ライブラリに executor は存在しない。// 以下のように脱糖(desugar)される。// `fn async_add_one(num: u32) -> impl Future<Output = u32>`asyncfnasync_add_one(num:u32)->u32{num+1}asyncfnexample_task(){letnumber=async_add_one(5).await;println!("5 + 1 = {}",number);}fnmain(){// futureは、作成された時点ではタスクは開始されない。letfuture=example_task(5);// JavaScriptと異なり、futureはポーリングされて初めて開始される。futures::executor::block_on(future);}
Async/awaitパターンをサポートする言語の大きな利点は、非同期の非ブロッキングコードを最小限のオーバーヘッドで記述でき、従来の同期ブロックコードとほとんど同じように見えることである。特にawaitは、メッセージパッシングプログラムで非同期コードを記述する最良の方法であると主張されてきた。特に、ブロッキングコードに近いため、読みやすさと定型コードの最小量が利点として挙げられた[15] 。その結果、async/awaitを使用すると、ほとんどのプログラマーがプログラムについて推論しやすくなり、awaitは、それを必要とするアプリケーションでより優れた、より堅牢なノンブロッキングコードを促進する傾向がある。このようなアプリケーションは、グラフィカルユーザインタフェースを提供するプログラムから、ゲームや金融アプリケーションなど、非常にスケーラブルなステートフルなサーバー側プログラムまでさまざまである。
awaitが批判されるときには、awaitは周囲のコードも非同期になる傾向があることがしばしば指摘される。一方で、このコードの伝染性(「ゾンビウイルス」と比較されることもある)はあらゆる種類の非同期プログラミングに固有であると主張されてきたため、この点に関してはawaitだけに特有のものではない[7]。
Taskを返すメソッドの名前は、慣例的にAsyncの接尾辞が付けられる。