-
Notifications
You must be signed in to change notification settings - Fork 0
MS_AsyncAwait
- 戻る(非同期処理)
-
async/await の登場で、マルチスレッド処理として実装しなくても、
非同期処理を同期型処理と、ほぼ変わらない記述で容易に実装可能になった。 -
await の意味は、
- スレッドを止めずノンブロッキングで
- 非同期から同期に復帰する
という意味らしい。
-
実際、async/await はそういう動きをする。
-
スレッドや Windows メッセージングキューで Task 化される。
昔懐かしい、ノンプリエンプティブ・マルチタスク(Win3.1)のようなイメージ。 -
しかし、その実態はとても複雑(詳しくはコチラを参照)。
- 内部がどう動いているか詳しく把握しないとシステムは安定しない。
- 中身をシッカリ理解して使用している人が少ないので注意が必要。
-
補足(一言でいうと「継続の分割」): async/await の本質は、
コンパイラがメソッドをステート マシンに書き換えることにある。【書いたコード】 var a = await FooAsync(); Console.WriteLine(a); 【コンパイラが生成するもの】 ① FooAsync() を呼ぶ ② まだ終わっていなければ、「続き」をコールバックとして登録して return (=ここでスレッドは解放される) ③ 完了したら、「続き」(Console.WriteLine)が実行される原文が「ノンプリエンプティブ・マルチタスクのようなイメージ」と
言っているのは的確で、OS がスレッドを横取りするのではなく、
awaitの位置でプログラム側が自発的に制御を手放す点が同じである。重要な帰結:
awaitは待たない。
その場で return して、続きを後で実行する。
だからスレッドが空き、スケーラビリティが上がる。
主に、非同期処理(≠並列処理)を簡単に(同期処理的に)実装するために導入された。
-
UI スレッドからのサーバ呼出のハングアップを防止するために使用される。
-
または、内部的には、UI スレッドから、時間のかかるバックグラウンド処理
(ネットワーク バインドまたは I/O バインドの処理)を分離する。 -
Web サーバのスレッド枯渇を防ぐ非同期 Controllerなどにも応用されている。
-
並列実行処理を実装するための基盤ではない。
-
Fire & Forgetや Task.WhenAll で並列実行処理を実装できるが、
await、Task.Wait() の "待ち合わせ" までがセットになっている仕組みなので、
そもそも、並列実行処理を実装することに特化した基盤ではないことに注意する。
(非同期タスクを作成して待つ処理を同期的に書ける仕組みと考えるべきか。) -
並列実行処理を実装する場合は、Thread や、ThreadPool を使用すればイイ。
-
補足(「非同期 ≠ 並列」の要点): 原文が繰り返し強調している通り、
ここが最大の誤解ポイントである。
非同期(async/await) 並列(Parallel / Thread) 目的 待ち時間にスレッドを解放 CPU を使い切る 対象 I/O バウンド(DB、HTTP、ファイル) CPU バウンド(計算) スレッド むしろ減る(1 本も要らないことがある) コア数だけ使う 代表 await httpClient.GetAsync()Parallel.For、Task.Run「async は速くするための道具ではない」。
1 件の処理は、むしろステート マシンのぶんわずかに遅くなる。
速くなるのは**同時に捌ける数(スループット)**である。現在の並列処理の道具は次の通り。
用途 手段 CPU バウンドの分割 Parallel.For/Parallel.ForEach非同期処理の並列度制御 Parallel.ForEachAsync(.NET 6+)複数の非同期を待ち合わせ Task.WhenAll生の並列 Thread(現在はほぼ使わない)なお
Task.Runは「並列」側の道具であり、
I/O 待ちをTask.Runで包むのは無意味(むしろ有害)である。
async/await の登場で、非同期処理を同期型処理と、ほぼ変わらない記述で容易に実装可能になった。
-
async で修飾した Task を返す非同期メソッドから、await ステートメントを付与して呼び出す。
-
若しくは、Task.Run() で実行して、Task.Wait() で待ち合わせる。
しかし、デバッグの時は非同期で実行されていることを意識する必要がある。
(しかも、かなり特殊な。同期コンテキスト毎に動作も大きく異なる。)
-
非同期化からのノンブロッキングの復帰には同期コンテキストが使用される。
-
同期コンテキストには Windows メッセージングキュー、ThreadPool などがある。
-
仕組みの詳細についてはコチラを参照。
async/await は、TAP(Task-based Asynchronous Pattern)の方式で実装されている。
async/await は、
- メッセージ(番号)のような制約のある単純固定長値のキューではなく、
実行コードの任意のコード断片そのものをキュー(同期コンテキスト)を使用して繋いでいる。 - .NET 3.5 用の async/await 互換 NuGet パッケージでは、
.NET 4 の Task 互換クラスを、内部を BeginInvoke 等で実装して、async/await を使えるようにしていた。
補足(非同期パターンの世代): .NET の非同期には 3 世代ある。
世代 名称 形 1 APM(Asynchronous Programming Model) BeginXxx/EndXxx2 EAP(Event-based Asynchronous Pattern) XxxAsync+XxxCompletedイベント3 TAP(Task-based Asynchronous Pattern) Task/Task<T>を返す現在は TAP のみを使う。APM / EAP は既存 API の互換のために残る。
async/await の夫々の意味。
-
async 修飾子
async は、呼び出し元と同じ同期コンテキストで実行されることを示す。 -
await 演算子
async を await すると、スレッドを止めずに同期コンテキストで同期する。
この動作は想像し難いが、具体的には、
- 同じ同期コンテキスト(Windows メッセージングキュー)に
- コールバックを順番に並べる(Control.BeginInvoke() 的な)。
というイメージ。
-
・・・結局、APM、EAP と同じ技術(同期コンテキスト)を使っている。
-
なお、同期コンテキストの種類によって、動きも異なる。
-
同期コンテキストはキュー的なもので実装されている。
例えば、前述の、- Windows メッセージングキュー、
- ThreadPool
- I/O 完了ポート
- , etc.
-
なので、言葉尻だけで動作を想像し難く、
利用の際は注意が必要になってくる。
-
移行メモ(
async修飾子の正確な意味): 原文の
「async は、呼び出し元と同じ同期コンテキストで実行されることを示す」
は、結果としての挙動を述べたものだが、
async修飾子自体にはその意味はない点を補足しておく。
asyncが行うのは、次の 2 つだけである。
- メソッド内で
awaitを使えるようにする- コンパイラにステート マシンへの書き換えを指示する
「同じ同期コンテキストに戻る」のは
await側の既定動作であり、
ConfigureAwait(false)で無効化できる(後述)。また、
asyncを付けてもメソッドはその場では非同期にならない。
asyncメソッドは呼び出されたら、まず同期的に実行される。
最初の「未完了のawait」に到達して初めて呼び出し元に戻る。
await(Task.Run)を切れ目として、
プログラマが意識して時間のかかるバックグラウンド処理
(ネットワーク バインドまたは I/O バインドの処理)を分割する。
- await 前の処理はフォアグラウンド的に実行される。
- await で呼び出す非同期メソッドはバックグラウンド的に実行される。
- await 後の処理は、コールバックとして実装せずにフォアグラウンドに復帰する。
- 厳密に言うとフォアグラウンドに復帰ではなく、
プログラムのコード上、非同期処理の後続処理に復帰する。 - 例えば、非同期 Controllerの非同期処理の後続処理は、
- 非同期スレッド側で実行され、最後にリクエストを受け付けたスレッドにバインドされる。
- この動作は、システム的には、フォアグラウンドに復帰するとは言えない。
- 厳密に言うとフォアグラウンドに復帰ではなく、
-
Task.Run 前の処理はフォアグラウンド的に実行される。
-
Task.Run() で呼び出す非同期メソッドはバックグラウンド的に実行される。
-
Task.Wait() 後の処理は、コールバックとして実装せずして、フォアグラウンドに復帰する。
-
Task.Wait が呼ばれるとスレッドは Task が終わるまで待機する。
-
これらの動きの詳細は、同期コンテキストによって異なってくる。
-
また、Task.Wait を使用すると、同期コンテキスト上、
実行する非同期 Task の前に、Task.Wait が割り込むとデッドロックになったりする。
Fire & Forgetの場合、戻り値は不要。
Awaitableの場合、
-
戻り値の無い非同期メソッドを実装する場合、Task 型の戻り値が必要。
-
基本的に、return を書く必要はない。
-
しかし、非同期メソッド内で非同期処理を呼び出さない場合、
Task.FromResultで、完了状態の Task を生成する return を書く必要がある。
await Task.FromResult(0);
await Task.FromResult(default(object));Awaitableの場合、
-
T 型の戻り値の有る非同期メソッドを実装する場合、Task<T> 型の戻り値が必要。
-
戻り値は Task<T> 型だが、await した後、T の型の値を return するように実装する。
int ret = await XXXXAsync();
return ret;- しかし、非同期メソッド内で非同期処理を呼び出さない場合、
Task.FromResultで、完了状態の Task<T> を生成する return を書く必要がある。
await Task.FromResult(new T());Task.FromResult を使用すると、完了状態の Task を生成することができる。
-
以下のようなケースで利用する。
- 戻り値の Task が長いコード パスを実行することなく、直ぐ完了する条件に合致する場合
- インターフェイス上は Task を返すが、メソッド内で非同期処理を呼び出さない場合
-
詳細については、下記を参照のこと。
- Task.FromResult(TResult) メソッド (System.Threading.Tasks)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.fromresult - c# - If my interface must return Task what is the best way to have a no-operation implementation? - Stack Overflow
https://stackoverflow.com/questions/13127177/if-my-interface-must-return-task-what-is-the-best-way-to-have-a-no-operation-imp
- Task.FromResult(TResult) メソッド (System.Threading.Tasks)
補足(現在の書き方/最新化): 原文の
「非同期処理を呼び出さないメソッド」の書き方は、現在では改善されている。// 原文の書き方(async を付けて、無駄に await する) public async Task DoAsync() { await Task.FromResult(0); } // 現在の書き方:async を付けず、完了済みタスクを返すだけ public Task DoAsync() { return Task.CompletedTask; } public Task<int> GetAsync() { return Task.FromResult(42); }
asyncを付けるとステート マシンが生成されるため、
中でawaitしないなら付けない方が速く、確保も減る。
用途 使うもの 戻り値なしの完了済み Task.CompletedTask(.NET 4.6+)戻り値ありの完了済み Task.FromResult(v)例外を返す Task.FromException(ex)キャンセル済み Task.FromCanceled(token)同期完了が多い高頻度処理 ValueTask<T>(アロケーションを避ける)
ValueTask<T>は「ほとんど同期的に完了するが、
たまに非同期になる」処理(キャッシュ ヒットなど)で
Taskの確保コストを避けるための型である。
ただし2 回 await できない等の制約があるため、
公開 API の既定はTaskのままでよい。
-
Task の実行が完了したら、待機せずに、
フォアグラウンドに復帰する風な動きを見せる。- await 非同期メソッド()
- await Task.Run()
-
await 演算子の使い方
- 非同期メソッドを呼び出すときに、await 演算子を利用する。
- メソッド内で await 演算子を利用する場合、async 修飾子でメソッドを修飾する。
- await 演算子は、async 修飾子の付くメソッドの中で1つ以上記述できる。
-
Task.Run() メソッド
- 非同期メソッドを実行して Task、Task<TResult> を返す。
-
Task.Wait()
- Task、Task<TResult> の実行が完了するまで待機する。
- async/await と異なり、同期コンテキストで同期せず、
待機(ブロック)した後にフォアグラウンドに復帰する。
-
Task.WhenAll()
- メソッドで複数のタスクを待機するタスクを取得する。
- この Task を Task.Wait() すると Task 毎の
例外を AggregateException 型として取得可能。
補足(例外の取り出され方が違う):
Task.WhenAllは、
待ち方によって受け取れる例外の数が変わる。var t = Task.WhenAll(t1, t2, t3); // ① await → 最初の 1 つの例外だけが throw される await t; // ② 全部を見たいなら、Task から取り出す try { await t; } catch { foreach (var e in t.Exception.InnerExceptions) Log(e); }
awaitはAggregateExceptionを「ほどいて」最初の例外を投げる
(同期処理と同じ書き味にするための仕様)。
全件を扱いたい場合は上記 ② のようにTask.Exceptionを見る。また、
WhenAllは一部が失敗しても、全部が終わるまで待つ。
「1 つでも失敗したら即座に打ち切りたい」場合は
CancellationTokenSourceと組み合わせる。
-
[.NET Framework 4/4.5][C# 5] 非同期で動くビジネスロジックを作る
: biac の それさえもおそらくは幸せな日々@nifty
http://bluewatersoft.cocolog-nifty.com/blog/2011/12/net-framework-4.html -
Task.Start()、Task.Factory.StartNew()、Task.Run()
- Task クラスと TaskFactory クラス
http://www.kanazawa-net.ne.jp/~pmansato/parallel/parallel_taskfactory.htm - Task.Start()とTask.Run()とTask.Factory.StartNew()の違い - From Full To Free
http://fromfulltofree.blog.fc2.com/blog-entry-7.html
- Task クラスと TaskFactory クラス
-
[雑記] スレッド プールとタスク
C# によるプログラミング入門 | ++C++; // 未確認飛行 C
http://ufcpp.net/study/csharp/misc_task.html -
Provide an asynchronous version method for call the B-layer
from the action method of asynchronous controller.
· Issue #216 · OpenTouryoProject/OpenTouryo
https://github.com/OpenTouryoProject/OpenTouryo/issues/216
ここでは、
- 「非同期メソッドの種類」と
- 「同期コンテキスト」の
組み合わせによって、await 後の処理が、
どのように実行されるのかについて説明する。
- 参考
- async/awaitと同時実行制御 | ++C++; // 未確認飛行 C ブログ
https://ufcpp.wordpress.com/2012/11/12/asyncawait%E3%81%A8%E5%90%8C%E6%99%82%E5%AE%9F%E8%A1%8C%E5%88%B6%E5%BE%A1/- An other world awaits you
http://www.slideshare.net/ufcpp/an-other-world-awaits-you
- An other world awaits you
- async/awaitと同時実行制御 | ++C++; // 未確認飛行 C ブログ
非同期メソッドには、2 種類ある。
-
Fire & Forget
戻り値が void のメソッド -
Awaitable
戻り値が Task(もしくは Task<T>)のメソッド
非同期メソッドでも return 文を書く必要が無い。
戻り値が void なので非同期以降がフォアグラウンドに復帰しない。
- 非同期呼び出しで投げっぱなす場合。
- 具体的には、GUI アプリケーションのイベント・ハンドラに適用する場合。
- 上記以外の用途での利用は意味もなく、ハマる原因になるので非推奨。
補足(
async voidが危険な理由): 後述のガイドラインとも重なるが、
async voidは単に不便なのではなく、危険である。① 例外を捕捉できない → try-catch で囲んでも捕まらない → 同期コンテキストに直接投げられ、プロセスが落ちる ② 完了を待てない → テストできない。終了処理と競合する ③ 呼び出し側が「終わったか」を知る手段が一切ない① が最も重い。
async Taskなら例外は Task に格納されるが、
async voidではキャッチする場所がないまま
スレッド プールに投げられ、未処理例外としてプロセスを終了させる。// 危険:例外はここでは捕まらない try { FireAndForget(); } catch { /* 到達しない */ } async void FireAndForget() { throw new Exception(); }イベント ハンドラー以外では絶対に使わない。
どうしても投げっぱなしにしたい場合は、
async Taskにして、呼び出し側で明示的に例外を握る。_ = DoAsync().ContinueWith(t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
非同期メソッドは、Task、Task<T> をリターンする。
戻り値が Task(もしくは Task<T>)なので、await 演算子か Wait() メソッド
以降に実装された非同期以降がフォアグラウンドに復帰する。
- 非同期呼び出しの呼び出し元で
- await で、非同期以降をフォアグラウンドに復帰させる場合。
- Task.Wait() で呼び出し元が非同期メソッドの完了を待機する必要がある場合。
実行環境によって持つ同期コンテキストの種類が異なる。
GUI アプリケーションでの同期コンテキストは
「Windows メッセージングキュー(Control.Invoke、.BeginInvoke で使う)」になる。
-
Fire & Forget
GUI アプリケーションの await の次の処理は、
同期コンテキストの Control.BeginInvoke によって、
UI スレッド上でシーケンシャルに実行される。 -
Awaitable
該当なし?Control.Invoke では?
Console アプリケーションでの同期コンテキストは「null」になる。
-
Fire & Forget
非推奨(Thread や、ThreadPool を使用すればイイ) -
Awaitable
- Console アプリケーション内の await の次の処理が実行されるスレッドは一意ではなくなる。
- ただし、処理自体は、(記述した順に)シーケンシャルに実行される。
Task.Run を使用した場合の同期コンテキストは、
マルチスレッド環境下の「ThreadPool」になる。
-
Fire & Forget
非推奨(Thread や、ThreadPool を使用すればイイ) -
Awaitable
- Task.Run 内の await の次の処理が実行されるスレッドは一意ではなくなり、
- 且つ、Task.Run 内の await の次の処理は、UI スレッドに戻らなくなる。
- ただし、処理自体は、(記述した順に)シーケンシャルに実行される。
ASP.NET アプリケーションでの同期コンテキストは
マルチスレッド環境下の「System.Threading.SynchronizationContext.Current」になる。
-
Fire & Forget
非推奨(Thread や、ThreadPool を使用すればイイ) -
Awaitable
- ASP.NET アプリケーションの await の次の処理が実行されるスレッドは一意ではなくなる。
- ただし、処理自体は、(記述した順に)シーケンシャルに実行される。
- HttpContext 等の保持、非同期処理が全て終わるまで、レスポンスしないよう監視
- 最終的に結果は、リクエストを受け付けたスレッドにバインドされる。
詳しくは、非同期 Controllerを参考にする。
この辺の
- asp.net mvc - MVC Action Filters Collection was modified;
enumeration operation may not execute - Stack Overflow
http://stackoverflow.com/questions/29429526/mvc-action-filters-collection-was-modified-enumeration-operation-may-not-execut
スタック・トレースを見ると、
- 「SynchronizationContext」と
- 「AsyncControllerActioninvoker」は
同じ事らしいと解る。
なお、恐らく、これらの同期コンテキストは、
I/O 完了ポート(IOCP)= ノンブロッキング I/O であると思われる。
- Can ASP.NET MVC's AsyncController be used to service large number of
concurrent hanging requests (long poll)? - Stack Overflow
http://stackoverflow.com/questions/4982882/can-asp-net-mvcs-asynccontroller-be-used-to-service-large-number-of-concurrent
補足(ASP.NET Core では同期コンテキストが無い/最新化): 本節は
**.NET Framework 時代の ASP.NET(System.Web)**を前提としている。
ASP.NET Core では状況が大きく変わったので、要点を補う。
ASP.NET (System.Web) ASP.NET Core 同期コンテキスト AspNetSynchronizationContextありnull(存在しない) await後のスレッド元のリクエスト スレッドにバインド ThreadPool の任意のスレッド HttpContext同期コンテキストが持ち回る IHttpContextAccessor(AsyncLocal).Result/.Wait()デッドロックする デッドロックはしない(が枯渇する) ConfigureAwait(false)必須級 不要(効果がない) ASP.NET Core で同期コンテキストが撤廃されたことにより、
本ページが詳述しているデッドロック問題の多くは
構造的に解消した。ただし
.Result/.Wait()が安全になったわけではない。
デッドロックはしなくなったが、
スレッドを 1 本占有して待つため、
負荷が高まるとスレッド プール枯渇を起こす。
同期的にブロックしてはならないという結論は変わらない。なお、同期コンテキストが残るのは主に UI
(WPF / WinForms / MAUI)であり、
そこでは本節の議論が今も有効である。
- GUI 以外の同期コンテキストで Fire & Forget を実行した場合(非推奨)や、
- Task.WhenAll で複数の Task を Task.Wait() した場合(ThreadPool で実行される)では、
並列実行を始めるので、
特に、Console アプリケーションの同期コンテキスト(null)の下では、
新規に同期処理の実装が必要になることがある。
async/await は非同期呼び出しで投げっぱなした後に、
同期コンテキストにより同期される方式のため、同期をあまり考慮していないが、
同期コンテキストによっては、同期を行う必要があるため、以下に注意する。
スレッドを使用した処理を記述しないが、
分割されたタスクは、
- 同一スレッドで動作することも
- 別スレッドで動作することもある。
このため、一連の処理が(分割されたタスクが)、
- 異なるスレッドで実装される保証は無い。
- 同じスレッドで実行される保証も無い。
従って、スレッド同期の lock 等は無意味。
従来のスレッド同期ツールキットは使用不可。
旧プログラムで、
- スレッド・アフィニティのあるロック機構(lock/mutex/semaphore)を使用してコードブロックをロックしている場合に、
- await を使用して修正変更をしたい場合(await 演算子を含むコードブロックをロックしたい場合)、
スレッド・アフィニティのないロック機構を使用する。
補足(
lockの中ではawaitが書けない): 正確には、
C# の構文としてlockブロック内にawaitは書けない
(コンパイル エラーになる)。lock (_sync) { await DoAsync(); // ← コンパイル エラー }理由は原文の言う通り、
Monitor(lockの実体)が
スレッド・アフィニティを持つ(取得したスレッドしか解放できない)ため。
awaitの前後でスレッドが変わり得る以上、成立しない。代替は
SemaphoreSlim(下記参照)。private readonly SemaphoreSlim _lock = new(1, 1); await _lock.WaitAsync(); try { await DoAsync(); } finally { _lock.Release(); }
finallyでのRelease()は必須(例外時に永久ロックになる)。なお、.NET 9 で
System.Threading.Lock型が追加され、
lock文がこの型に対応したが、
これも同期用であり、awaitは書けない点は変わらない。
Win32 の待機関数の WaitFor[Single|Multi]Object は例外的に使用可。
これらの待機関数は、
- スレッド・アフィニティではなく、
- 「状態変化」(ノンシグナル状態からシグナル状態への変化)を
待つ関数であるためと考える。
- スレッド・アフィニティのないロック機構
- .NET TIPS:非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)[C#、VB] - @IT
http://www.atmarkit.co.jp/ait/spv/1411/11/news117.html- SemaphoreSlim.WaitAsync 関数も async/await する。
- Semaphore による lock さえ、タスクとして分割されて待機/実行される。
- ThreadPool クラス (System.Threading)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.threadpool- ThreadPool.RegisterWaitForSingleObject メソッド (System.Threading)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.threadpool.registerwaitforsingleobject
- ThreadPool.RegisterWaitForSingleObject メソッド (System.Threading)
- EventWaitHandle クラス (System.Threading)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.eventwaithandle- AutoResetEvent クラス (System.Threading)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.autoresetevent - ManualResetEvent クラス (System.Threading)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.manualresetevent
- AutoResetEvent クラス (System.Threading)
- .NET TIPS:非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)[C#、VB] - @IT
補足(ただし「ブロックする待機」である点は変わらない): 原文の
「WaitFor[Single|Multi]Object は例外的に使用可」は
スレッド・アフィニティの観点では正しいが、
待っている間スレッドを占有する点は同じである。非同期文脈では、次の待たない待機を使うのが定石。
待ちたいもの 非同期版 排他制御 SemaphoreSlim.WaitAsync()シグナル TaskCompletionSource<T>時間 Task.Delay()(Thread.Sleepは不可)一斉開始 TaskCompletionSource+Task.WhenAll生産者・消費者 System.Threading.Channels
TaskCompletionSource<T>は
「外部のイベントをawait可能にする」ための汎用手段で、
WaitHandleを待つ場合も
ThreadPool.RegisterWaitForSingleObjectと組み合わせて
Task化するのが現在の書き方である。
上記の仕組みで動いているとすると、進捗報告をどう実装するかが?であるが、
以下を見ると、Progress クラス、IProgress<T> インターフェースを使用するらしい事が解る。
-
非同期メソッド - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
http://ufcpp.net/study/csharp/sp5_async.html#cancel -
.NET TIPS:WPF/Windowsフォーム:
時間のかかる処理をバックグラウンドで実行するには?(async/await編)[C#/VB] - @IT
http://www.atmarkit.co.jp/ait/articles/1512/02/news019.html
詳しい仕組みは不明だが、GUI 上で動作しているため、
同期コンテキストの Control.Invoke、.BeginInvoke を使用しているものと思われる。
補足(
Progress<T>の仕組み): 原文の推測は正しい。
Progress<T>はコンストラクタで実行時の同期コンテキストを捕捉し、
Report()されたらそのコンテキストにポストする。// UI スレッドで生成する(=ここで UI の同期コンテキストを捕まえる) var progress = new Progress<int>(p => progressBar.Value = p); await DoWorkAsync(progress); // ワーカー側は Report するだけでよい要点:
Progress<T>はUI スレッドで生成すること。
バックグラウンドで生成すると同期コンテキストが null になり、
コールバックが UI スレッドで動かず、例外になる。なお、ASP.NET Core など同期コンテキストが無い環境では
常に ThreadPool 上で呼ばれる点にも注意。
-
.NET 4.0 から CancellationToken を利用したタスクのキャンセル機構が導入された。
- タスク開始前にトークンがキャンセルされてるかのチェックするため、タスクを開始するコストが減る。
- ThrowIfCancellationRequested メソッドで OperationCanceledException(OCE)をスローした場合、
- タスクの本体が OCE に含まれるキャンセルトークンも監視しており、
- 同じだったら、タスクのステータスは Canceled になる(普通は、Faulted)。
-
参考
-
【C#】Taskをキャンセルする - vaguely
http://mslgt.hatenablog.com/entry/2017/10/08/091841 -
【C#】タスクのキャンセル方法 - Tumbling Dice
http://outofmem.hatenablog.com/entry/2014/04/02/014201 -
C# Taskの引数に使うCancellationTokenは何に使われているのか? - 株式会社リッカ
- c# - Cancellation token in Task constructor: why? - Stack Overflow
https://stackoverflow.com/questions/3712939/cancellation-token-in-task-constructor-why
- c# - Cancellation token in Task constructor: why? - Stack Overflow
-
Microsoft Learn
- マネージド スレッドのキャンセル
https://learn.microsoft.com/ja-jp/dotnet/standard/threading/cancellation-in-managed-threads - CancellationToken Struct (System.Threading)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.cancellationtoken
- マネージド スレッドのキャンセル
-
Qiita
- Cancellation Token について調べてみる
https://qiita.com/TsuyoshiUshio@github/items/b2d23b37b410a2cfd330 - 非同期で複数処理を実行し、対話式で制御する
https://qiita.com/haminiku/items/cc299c1ed94d7ba3f9ec
- Cancellation Token について調べてみる
-
補足(実装の指針):
CancellationTokenは
公開する非同期メソッドの末尾引数として受け取るのが慣習である。public async Task<T> GetAsync(string id, CancellationToken ct = default) { var res = await _http.GetAsync(url, ct); // 下流へ必ず渡す ct.ThrowIfCancellationRequested(); // 自前ループでは明示的に確認 ... }要点:
- 受け取ったトークンは必ず下流に流す(途中で握り潰さない)
- キャンセルは例外(
OperationCanceledException)で伝わる。
これをcatch (Exception)でまとめて握るとキャンセルが効かなくなる- タイムアウトも同じ仕組み
(new CancellationTokenSource(TimeSpan.FromSeconds(5)))- ASP.NET Core では
HttpContext.RequestAbortedが
クライアント切断時にキャンセルされるトークンとして使える.NET 8 では
CancellationTokenSource.CancelAsync()も追加された。
-
複数のタスクを継続に連結し、時間のかかる同期コンテキストを経由しない。
-
参考
- 方法: 複数のタスクを継続に連結する
https://learn.microsoft.com/ja-jp/dotnet/standard/parallel-programming/how-to-chain-multiple-tasks-with-continuations - Task.ContinueWith メソッド (System.Threading.Tasks)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.continuewith
- 方法: 複数のタスクを継続に連結する
補足:
ContinueWithはasync/await以前の書き方であり、
既定のスケジューラが直感に反する(TaskScheduler.Currentを使う)等の
落とし穴があるため、新規コードではawaitを使う。
現在もContinueWithが要るのは、
「投げっぱなしタスクの例外を握る」など限られた場面のみ。
-
同期コンテキストを使用するか・しないかを制御できる。
-
参考
- Task.ConfigureAwait メソッド (Boolean) (System.Threading.Tasks)
https://learn.microsoft.com/ja-jp/dotnet/api/system.threading.tasks.task.configureawait
- Task.ConfigureAwait メソッド (Boolean) (System.Threading.Tasks)
補足(現在の指針):
ConfigureAwait(false)は
「awaitの後で、元の同期コンテキストに戻らなくてよい」
という指示である。
場所 指針 ライブラリ ConfigureAwait(false)を付ける(呼び出し元を選ばない)UI アプリ 付けない(UI 更新のため戻る必要がある) ASP.NET Core 不要(同期コンテキストが無いため効果がない) コンソール 不要 .NET 8 では
ConfigureAwait(ConfigureAwaitOptions)が追加され、
SuppressThrowing(例外を投げずに待つ)等も指定できるようになった。
以下の参考資料から、ガイドラインを纏めてみた。
- 呼び出し側でタスクの終了を検出することができない。
- タスクで発生した例外を呼び出し側で補足することができない。
- 例外:イベントハンドラは OK(そもそもイベントハンドラ用)。
→ 前項の理由のような挙動でも問題ないが無いため。
- Task.Wait() は、非同期処理を待機するため、デッドロックの原因になり易い。
- 実行する非同期 Task の前に、Task.Wait が割り込むとデッドロックになったりする。
以下、デッドロックのサンプル。
-
UI の場合のサンプル
同期コンテキストである Windows メッセージングキューに、
Task.Wait、非同期 Task の順でキューイングされるため
前者が後者の完了を待ち続け、後者が何時迄も実行されないため(と思われる)。- [C#] 同期メソッドから非同期メソッドを呼び出すと
アプリケーションがフリーズする - 非同期メソッド呼び出しによるデッドロック
https://www.ipentec.com/document/document.aspx?page=csharp-deadlock-call-async-method-from-sync-method
- [C#] 同期メソッドから非同期メソッドを呼び出すと
-
ASP.NET の場合のサンプル
同期コンテキストである I/O 完了ポートに、
Task.Result(≒Task.Wait)、非同期 Task の順でキューイングされるため
前者が後者の完了を待ち続け、後者が何時迄も実行されないため(と思われる)。- ASP.NET で非同期 (Async) を乗りこなす – Tsmatz
https://tsmatz.wordpress.com/2012/05/08/asp-net-mvc-async/- await で Post された同期処理は、GetHeader().Result(非同期処理) の処理が終わるまで待機。
- 同時に、GetHeader().Result(非同期処理) は、この Post された同期処理を待機。
- ASP.NET で非同期 (Async) を乗りこなす – Tsmatz
-
参考
- Task.Waitの問題点
http://outside6.wp.xdomain.jp/2016/08/06/post-343/ - async/await ~非同期なライブラリは楽じゃない~ - 飽きっぽい人のブログ@qwerty2501
http://qwerty2501.hatenablog.com/entry/2014/04/24/235849
- Task.Waitの問題点
補足(デッドロックの原理を一枚で): このデッドロックは
同期コンテキストが「1 つしか通さない」ことに起因する。① UI スレッド:DoAsync().Result で待機(UI スレッドを占有したまま) ↓ ② DoAsync 内の await が完了 → 「続き」を UI スレッドで実行しようとする ↓ ③ しかし UI スレッドは ① で塞がっている ↓ ④ ② は永久に実行されず、① も永久に待ち続ける → デッドロック解決は 2 つしかない。
.Result/.Wait()を使わず、最後までawaitする(推奨)- ライブラリ側で
ConfigureAwait(false)を付け、
② が UI スレッドに戻ろうとしないようにする(対症療法)**「async は下から上まで(async all the way)」**という原則は、
この構造から導かれる。途中で同期に戻してはならない。
ざっくり、以下のガイドラインに従う。
-
基本的に同期で実装する。
-
非同期は、async/await を使用しない従来の非同期で実装する。
- コールスタックの下位で async を使うと、呼出側も async の使用が必要になる。
- そのため、一番外側まで async を使うようにする必要がある。
- しかし、動作を変えることができないケースがあるので、
Task.Wait やその他のブロック手段を使って同期を取るしかなくなる。
-
async/await を使用する場合、
- Task を返すだけにして、Task.Run は使わないようにする。
- ライブラリ内で await する場合は、ConfigureAwait(false) を使う。
詳しくは、下記を参照のこと。
- アンチパターン
public static async Task FetchFileAsync(int fileNum)
{
await Task.Run(() =>
{
var contents = IO.DownloadFile();
Console.WriteLine("Fetched file #{0}: {1}", fileNum, contents);
});
}-
理由:
-
ライブラリがグローバル共有リソースである ThreadPool を使用することになる。
-
ライブラリは、実行コンテキストが不明なので、ThreadPool 利用の決定は、
ライブラリ開発者ではなくアプリケーション開発者がするべき。 -
ライブラリが非同期メソッドを提供するのはネイティブ非同期メソッドを使用する場合。
- ネイティブ非同期メソッドはスレッドプールを使った別スレッドによる非同期処理を目的としていない。
- I/O などの待ちに対してスレッドを空けて同時実効性を高めることが目的。
-
-
サーバでの Task.Run
- サーバで Task.Run を使わない。
- 理由:
- Task.Run はスケーラビリティが求められるサーバでは不適切
- I/O バウンドの場合(CPU バウンドでない場合)だけ、非同期タスクを定義する意味があるが、
この決定はライブラリ開発者ではなくアプリケーション開発者がするもの。
-
クライアントでの Task.Run
- クライアントでも、Task.Run を使わない。
- 理由:
- クライアント側では Task.Run を使う理由がたくさんある。
- しかし前述にあるように、実行コンテキストが不明なので、ライブラリ内で Task.Run を使わない。
-
例外
-
例外: マルチスレッドと WinJS
WinJS は新しいバックグラウンドスレッドを作ることができないので、かわりにライブラリ側で作る必要がある。 -
例外: Stream.ReadAsync
- ある種のストリームはこれをサポートしない。
- サポートされない場合は、基底クラス(Stream クラス)で Task.Run を実行するのが最も安全な方法
-
移行メモ(誤記): 原文の「非同期 t タスク」は
非同期タスクの誤記、「Steam クラス」は Stream クラス の誤記と
判断し修正した。
補足(「偽の非同期」というアンチパターン): この節が指摘している
Task.Runでラップしただけの非同期は、
fake async / async over sync と呼ばれる代表的なアンチパターンである。【本物の非同期(I/O)】 HTTP 要求を出す → スレッドを返す → 応答が来たら続きを実行 → 待っている間、スレッドは 0 本 【Task.Run で包んだだけ】 別のスレッドを 1 本借りて、そこでブロックして待つ → 消費するスレッドは減っていない。むしろ切り替えのぶん増えるサーバでは特に有害で、
「非同期にしたのにスループットが上がらない」
「むしろスレッド プールが枯渇した」の原因になる。逆に、CPU バウンドの処理を UI スレッドから逃がす目的で
アプリケーション側がTask.Runを使うのは正しい用途である。
原文の主張は「ライブラリが勝手に決めるな」という点にある。
-
これは以下の様なユーザの仮定に基づくため。
-
同期バージョンの方が非同期バージョンより高速(と言う仮定)。
非同期バージョンより高速な同期バージョンを提供できない場合、- 両方のバージョンを提供する(=ラップを提供する)理由が無い。
- 非同期バージョンを呼び出すときに Task.Wait を使って同期をとるほうが良い。
-
同期バージョンは、UI スレッドで実行しても安全(と言う仮定)。
非同期メソッドをラップした同期メソッドが Task.Wait を使っていた場合、デッドロックが発生する可能性がある。
-
-
従って、推奨は、
- メソッドが同期処理を行うなら、同期バージョンだけを提供する。
- メソッドが非同期処理を行うなら、
- 非同期バージョンだけを提供する。
- 高速に動作するデッドロックを起こさない同期メソッドのみ追加で定義可能。
-
基本的にはライブラリ内で await する場合は ConfigureAwait(false) する。
- 性能的に早くなる。
- UI の場合の同期コンテキストである Windows メッセージングキューなど、
同期コンテキストによっては、デッドロックさせる可能性が高くなる。
-
ConfigureAwait(false) すると元のスレッド(主に UI スレッド)には戻らない。
必要に応じて、同期コンテキストによる動作スレッドの切り替えを行う。
// UIスレッドの同期コンテキストをキャッシュする
SynchronizationContext syncContext = SynchronizationContext.Current;
//.ConfigureAwait(false)でUIスレッドに戻さない
await HeavyWorkAsync().ConfigureAwait(false);
// UIスレッドの同期コンテキストにディスパッチする。
syncContext.Post(state =>
{
// 何からの処理。
}, null);-
実行コンテキストをコピーする。
- ログイン・ユーザやカルチャ情報など偽装をする場合、
CallContext.SetLocalData を使用して、実行コンテキストをコピーする。 - この処理は非同期呼び出しに少量の性能コストを追加する。
- ログイン・ユーザやカルチャ情報など偽装をする場合、
-
ループ内で呼び出さない。
- async を使ったメソッドは Task の生成や Task の実行管理のためのコストがかかる。
- 従って、ライブラリのユーザにはループ内で呼び出さないように注意喚起する。
-
参考
- Async/Await- パフォーマンス上のオーバーヘッドと他の落とし穴
http://www.infoq.com/jp/news/2013/07/async-await-pitfalls- Async/Await - Best Practices in Asynchronous Programming
http://msdn.microsoft.com/en-us/magazine/jj991977.aspx
- Async/Await - Best Practices in Asynchronous Programming
- Async/Await- パフォーマンス上のオーバーヘッドと他の落とし穴
補足(
CallContextは .NET Core では使えない/最新化):CallContext
(System.Runtime.Remoting.Messaging)は .NET Framework 専用で、
.NET Core 以降では利用できない。移行先は
AsyncLocal<T>である。private static readonly AsyncLocal<string> _tenant = new(); _tenant.Value = "contoso"; // await をまたいで引き継がれる
ThreadLocal<T>AsyncLocal<T>単位 スレッド 非同期の流れ(実行コンテキスト) await後失われる 引き継がれる ASP.NET Core の
IHttpContextAccessorもこの仕組みで実装されている。
なお、原文の指摘通りコピーにはコストがあるため、
使いすぎない(DI で引き回せるならそちらを使う)方がよい。
非同期メソッドの呼び出しは次の 3 つのメモリ確保処理を生む。
- ◯:ローカルの変数を保存するためのステートマシン
- ◯:継続のためのデリゲート
- 結果を返すためのタスク
◯が付与された、ステートマシンとデリゲートは await キーワードが
ランタイムに現れたときに作成されるため、同期処理と比べるとコストになる。
補足(現在は大幅に改善されている/最新化): この節の記述は
.NET Framework 時代の実装を前提としている。
その後、次の改善が入った。
改善 内容 ステート マシンの構造体化 同期完了する場合、ヒープ確保が起きない ValueTask<T>同期完了時に Taskの確保を回避IValueTaskSourceTask オブジェクトの再利用(Socket 等で利用) Async ValueTask Pooling ステート マシンのプール(.NET 5+、既定オフ) 現在は、同期的に完了するパスではアロケーションがほぼ発生しない
ところまで最適化されている。ただし原文の結論「ループ内で大量に呼ばない」は依然有効で、
特に1 件ずつawaitする N+1 パターンは避けるべきである。// 悪い:1 件ずつ順番に待つ(N 回のラウンドトリップ) foreach (var id in ids) results.Add(await GetAsync(id)); // 良い:まとめて並行に投げる(並列度の制御は必要) var results = await Task.WhenAll(ids.Select(GetAsync)); // より良い:並列度を制限する(.NET 6+) await Parallel.ForEachAsync(ids, new ParallelOptions { MaxDegreeOfParallelism = 8 }, async (id, ct) => { ... });
-
C#の非同期の落とし穴
https://www.infoq.com/jp/news/2013/04/async-csharp-fsharp -
Async in C# and F#: Asynchronous gotchas in C# (Japanese translation)
https://gist.github.com/pocketberserker/5565303 -
neue cc
- asyncの落とし穴Part2, SynchronizationContextの向こう側
http://neue.cc/2013/07/02_412.html - asyncの落とし穴Part3, async voidを避けるべき100億の理由
http://neue.cc/2013/10/10_429.html
- asyncの落とし穴Part2, SynchronizationContextの向こう側
-
TAP (Task-based Asynchronous Pattern) 非同期メソッドのガイドライン - Qiita
http://qiita.com/chocolamint/items/ed4999cccf011653cb78 -
.NETで非同期ライブラリを正しく実装する
https://www.infoq.com/jp/articles/Async-API-Design- Creating Async Libraries That Are Modular, Reusable and Fast,
in Microsoft Visual C# and Visual Basic | TechEd Europe 2013 | Channel 9
https://channel9.msdn.com/Events/TechEd/Europe/2013/DEV-B318
- Creating Async Libraries That Are Modular, Reusable and Fast,
-
Tasks are (still) not threads and async is not parallel
http://blogs.msdn.com/b/benwilli/archive/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel.aspx
補足(要点の要約): 長いページなので、実務上の指針を 7 点に纏める。
asyncは下から上まで。途中で.Result/.Wait()に戻さない。async voidはイベント ハンドラーだけ。- ライブラリでは
ConfigureAwait(false)(ASP.NET Core では不要)。- I/O は本物の非同期 API を使う。
Task.Runで包まない。CancellationTokenを受け取り、下流へ流す。- 並列と非同期を区別する。CPU バウンドは
Parallel系。lockの中でawaitしない。SemaphoreSlimを使う。
-
Insider.NET > 業務アプリInsider
- 連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門 -@IT
http://www.atmarkit.co.jp/ait/subtop/features/dotnet/app/masterasync_index.html
- 連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門 -@IT
-
Taskを極めろ!async/await完全攻略 - Qiita
http://qiita.com/acple@github/items/8f63aacb13de9954c5da -
ASP.NET で非同期 (Async) を乗りこなす | Tsmatz
https://tsmatz.wordpress.com/2012/05/08/asp-net-mvc-async/
- 非同期プログラミング (C#)
https://learn.microsoft.com/ja-jp/dotnet/csharp/asynchronous-programming/ - タスク ベースの非同期パターン (TAP)
https://learn.microsoft.com/ja-jp/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap - ConfigureAwait FAQ
https://devblogs.microsoft.com/dotnet/configureawait-faq/
- TPL入門
http://blog.xin9le.net/entry/tpl-intro - 非同期メソッド入門
http://blog.xin9le.net/entry/async-method-intro
- 非同期処理(インデックス) - MakCraft
http://www.makcraft.com/devref/26-asynchronous-processing-index.html
- (その1)
http://www.makcraft.com/blog/meditation/2013/03/22/asynchronous-processing-part-1/ - (その2)
http://www.makcraft.com/blog/meditation/2013/03/23/asynchronous-processing-part-2/ - (その3)
http://www.makcraft.com/blog/meditation/2013/03/23/asynchronous-processing-part-3/ - (その4)
http://www.makcraft.com/blog/meditation/2013/03/24/asynchronous-processing-part-4/ - (その5)
http://www.makcraft.com/blog/meditation/2013/04/02/asynchronous-processing-part-5/
- プロデューサー/コンシューマー パターン
http://www.makcraft.com/blog/meditation/2013/03/24/producer-consumer-pattern/ - を利用したサーバー側接続処理
http://www.makcraft.com/blog/meditation/2013/04/04/server-side-connection-processing-using-the-producer-consumer-pattern/ - を利用した IPv4 及び IPv6 接続待ち
http://www.makcraft.com/blog/meditation/2013/04/06/listen-on-ipv6-and-ipv4-based-producer-consumer-pattern/
Tags: 移行, プログラミング, .NET開発
このWikiは「Open棟梁Project」,「OSSコンソーシアム 開発基盤部会」によって運営されています。