Task-based Asynchronous Pattern (TAP) と呼ばれる、最新の .NET Framework で推奨される非同期メソッド実装におけるデザインガイドラインをまとめました。にわかなのでマサカリ歓迎します。
Async および Await を使用した非同期プログラミング (C# および Visual Basic) も参照してください。
また、下記では .NETで非同期ライブラリを正しく実装する の内容を踏まえています。
メソッド名は ~Async
もしくは ~TaskAsync
とする
非同期操作を実行するメソッドは名前に Async
サフィックスをつけます。
もし既に Async
で終わる Task
を返却しない同名メソッド(IAsyncResult
などを返すメソッド)が存在している場合は TaskAsync
とします。
var result = await httpClient.GetAsync("http://google.co.jp/");
TAP の非同期操作は、操作名の後に
Async
サフィックスが付きます。たとえば、取得 (Get) 操作の場合はGetAsync
になります。 既にAsync
サフィックスの付いたメソッド名を含むクラスに TAP メソッドを追加する場合は、代わりにTaskAsync
サフィックスを使用します。 たとえば、既にクラスにGetAsync
メソッドが含まれている場合は、GetTaskAsync
という名前を使用します。
タスク ベースの非同期パターン (TAP)
例外
イベントハンドラで非同期処理を実行する場合は従来のイベントハンドラ名で構いません。
private async void btnExecute_Click(object sender, EventArgs e)
{
await foo.BarAsync();
DoSomething();
}
戻り値は Task
もしくは Task<T>
とする
非同期操作を実行するメソッドの戻り値は Task
もしくは Task<T>
を戻り値とします。
戻り値に void
は使用しません。
例外
イベントハンドラで非同期処理を実行する場合は void
で構いません。
イベントハンドラ以外で void
の非同期メソッドを作成してはいけません。
private async void btnExecute_Click(object sender, EventArgs e)
{
await foo.BarAsync();
DoSomething();
}
(参考) asyncの落とし穴Part3, async voidを避けるべき100億の理由
ライブラリとして提供するメソッドで Task.Run
は使用しません
Task.Run
を使用するかどうかは呼び出し元が決定できますし、するべきです。
すなわち、ライブラリが非同期メソッドを提供するのは主に内部でネイティブ非同期メソッドを使用する場合です。
public async Task<bool> NanikaHandan()
{
var result = await _httpClient.GetAsync(_toaruUri).ConfigureAwait(false);
var str = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return str == _nanika;
}
ネイティブ非同期メソッドはスレッドプールを使った別スレッドによる非同期処理を目的としていません。
I/Oなどの待ちに対してスレッドを空けて同時実効性を高めることが目的のため、Task.Run
を使用する非同期処理とは本質的に目的が異なります。
例外
.NETで非同期ライブラリを正しく実装する 内で触れられています。
ライブラリとして提供するメソッドで ConfigureAwait(false)
を呼び出します
呼び出し元の UI で Task.Wait()
を使用せざるを得ない場合にデッドロックになる現象を回避するためです。
これが有名な ConfigureAwait(false)
地獄です。有名なのかな……?
冒頭の記事のほか HttpClient詳解、或いは非同期の落とし穴について でも解説されています。
また、Web の場合(かつ処理の完了を待たないような書き方をするケースで)は SynchronizationContext
が null
になって NullReferenceException
が発生するという問題もあります。こちらは asyncの落とし穴Part2, SynchronizationContextの向こう側 で解説されています。
ちなみに、この辺は過去に識者たちが改めて解説してくださいました。
ConfigureAwait - Togetterまとめ
(2016/02/06 追記) ConfigureAwait(false)
をつけまくるかどうかについて
デッドロックを防ぐには以下の様な呼び出し方をする方法もあることを教えてもらいました。
Task.Run(async () => await hoge.FugaAsync()).Wait();
このようにすると hoge.FugaAsync()
が戻そうとする先の SynchronizationContext
が Task.Run
によって生成されたスレッドプールからのスレッドに対して向くため、Wait()
するスレッドと互いに待ち合う形にならず、デッドロックを防止できます。
以下の様なメリット・デメリットが考えられます。
- ライブラリ側が
ConfigureAwait(false)
をつけまくる- [○] 呼び出し元が
Wait()
を呼んでしまってもデッドロックしない - [△]
ConfigureAwait(false)
のつけ忘れがあるとデッドロックしてしまう - [○] 同期コンテキストを戻さない分、パフォーマンスが良い
- [×] ソースコードにノイズが増えて見づらくなる(横に長くなるし)
- [○] 呼び出し元が
- 呼び出し元で
Task.Run
して回避する- [○] ライブラリ側が書きやすくなる
- [×] パフォーマンス面では不利になる
- [×] 呼び出し元が
Wait()
する際にきちんと理解して回避しないとデッドロックする
要はライブラリ側で考慮してあげるか、利用側が考慮して呼び出すかという責務の問題になります。
ライブラリ開発者、利用者がそれぞれどのような人たちなのか、考えられる Wait()
せざるを得ない状況がどのくらいあるのか、等によって決めると良いかと思います。
(参考) Wait() をなるべく無くす工夫
catch
句で await
が使えない問題
C#5.0以前では catch
句で await
を使用することができません。
catch (HogeException ex)
{
// C#5.0 以前では await できない
await foo.HandleErrorAsync();
throw;
}
そのため Wait()
を呼び出さざるを得ないということもあるのではないでしょうか。
catch (HogeException ex)
{
// 同期版メソッドが用意されていない場合など
foo.HandleErrorAsync().Wait();
throw;
}
しかし、ExceptionDispatchInfo
を使用することで非同期メソッドを同期的に待機しなくて済むように工夫できるケースがあります。
ExceptionDispatchInfo info = null;
try
{
....
}
catch (HogeException ex)
{
// 例外情報を捕捉
info = ExceptionDispatchInfo.Capture(ex);
}
if (info != null)
{
// catch の外で処理する
await foo.HandleErrorAsync();
// Throw() で例外情報を保ったまま再スローできる
info.Throw();
}
可能であれば CancellationToken
を受け取ります
非同期処理をキャンセル可能とするために CancellationToken
を受け取るオーバーロードを作成します。
public async Task<bool> NanikaHandan(CancellationToken cancellationToken)
{
var result = await _httpClient.GetAsync(_toaruUri, cancellationToken).ConfigureAwait(false);
var str = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
return str == _nanika;
}
また、CancellationToken
を受け取らないオーバーロードから CancellationToken
を受け取るオーバーロードを呼び出す場合には CancellationToken.None
を使用できます。
public Task<bool> NanikaHandan()
{
return NanikaHandan(CancellationToken.None);
}
Task.Run
を使わない場合、自身でキャンセル要求に対する中断を実装することは少ないかもしれませんが、呼び出し元からキャンセルが要求されたかを確認して処理を中断したい場合には CancellationToken.ThrowIfCancellationRequested()
メソッドを使用します。
末尾の処理が非同期メソッドの呼び出しで、かつ唯一の場合は await を使用せずにそのまま返します
public async Task HogeAsync()
{
await fuga.DoSomethingAsync();
}
上記は下記のようにします。
不要な Awaitor
呼び出しが挟まらなくなります。
public Task HogeAsync()
{
return fuga.DoSomethingAsync();
}
ちょうど IEnumerable<T>
を foreach
で列挙して yield return
するなら、その IEnumerable<T>
をそのまま戻せばいいじゃない、と同じような話かと。
(2016/02/06 追記) とは言え…
実際には以下の様な修正が発生した時に変更量が多くなってしまうので微妙なところです。
public Task HogeAsync()
{
// ここに piyo.DoSomethingAsync(); を追加したい
return fuga.DoSomethingAsync();
}
↓
public async Task HogeAsync() // この行が Diff に :sob:
{
await piyo.DoSomethingAsync();
await fuga.DoSomethingAsync(); // この行も diff に :sob:
}
ということで、私は普段は「別のオーバーロードを呼び出す場合」くらいでしか「await
せずに直接 Task
を返却」ということをやっていません。別のオーバーロード呼び出しであれば前後に処理が増えることはほとんど発生しないためです。
(2016/02/12 追記) using を使う場合などは注意が必要
うっかりやりかねないので念のため書いておきます。
下記のようなコードは意図しない動作をするので注意が必要です。
public Task HogeAsync()
{
using (var foo = new Foo())
{
return foo.FugaAsync(); // foo.FugaAsync() の終了を待たずに foo.Dispose() が呼ばれる
}
}
これは以下と等価ではありません。
public async Task HogeAsync()
{
using (var foo = new Foo())
{
await foo.FugaAsync(); // foo.Dispose() は foo.FugaAsync() の待機後に呼ばれる
}
}
それぞれ(概ね)以下のように展開されるためです。
// 1 つ目のパターン
public Task HogeAsync()
{
// using は try-finally で Dispose するコードのシンタックスシュガー
Foo foo = null;
try
{
foo = new Foo();
return foo.FugaAsync(); // Task を返す
}
finally
{
// Task を返した直後に呼ばれる
if (foo != null)
foo.Dispose();
}
}
// 2 つ目のパターン
public Task HogeAsync()
{
// using 内で await するとコンパイラが実行順序をヨシナニしてくれる
var foo = new Foo();
// 実際は Awaiter 経由だし SynchronizationContext.Current が null のケースとかあるけど、イメージとしてはこんな感じ
return foo.FugaAsync().ContinueWith(t =>
{
foo.Dispose();
}, TaskScheduler.FromCurrentSynchronizationContext());
}
なお、もはや非同期の話ですらないですが、yield return
についても using
内で同じ話になるので注意。
public IEnumerable<FooBar> GetFooBars()
{
using (hoge)
{
return fooBars;
}
}
// ↑と↓は同義ではない
public IEnumerable<FooBar> GetFooBars()
{
using (hoge)
{
foreach (var fooBar in fooBars)
{
yield return fooBar;
}
}
}