MS_AsyncAwait - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

async/await

抂芁

はじめに

  • 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 / EndXxx
2 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 ぀だけである。

  1. メ゜ッド内で await を䜿えるようにする
  2. コンパむラにステヌト マシンぞの曞き換えを指瀺する

「同じ同期コンテキストに戻る」のは await 偎の既定動䜜であり、
ConfigureAwait(false) で無効化できる埌述。

たた、async を付けおもメ゜ッドはその堎では非同期にならない。
async メ゜ッドは呌び出されたら、たず同期的に実行される。
最初の「未完了の await」に到達しお初めお呌び出し元に戻る。

実装方法

タスク分割方法

awaitTask.Runを切れ目ずしお、
プログラマが意識しお時間のかかるバックグラりンド凊理
ネットワヌク バむンドたたは I/O バむンドの凊理を分割する。

await

  • await 前の凊理はフォアグラりンド的に実行される。
  • await で呌び出す非同期メ゜ッドはバックグラりンド的に実行される。
  • await 埌の凊理は、コヌルバックずしお実装せずにフォアグラりンドに埩垰する。
    • 厳密に蚀うずフォアグラりンドに埩垰ではなく、
      プログラムのコヌド䞊、非同期凊理の埌続凊理に埩垰する。
    • 䟋えば、非同期 Controllerの非同期凊理の埌続凊理は、
      • 非同期スレッド偎で実行され、最埌にリク゚ストを受け付けたスレッドにバむンドされる。
      • この動䜜は、システム的には、フォアグラりンドに埩垰するずは蚀えない。

Task.Run  Task.Wait()

  • Task.Run 前の凊理はフォアグラりンド的に実行される。

  • Task.Run() で呌び出す非同期メ゜ッドはバックグラりンド的に実行される。

  • Task.Wait() 埌の凊理は、コヌルバックずしお実装せずしお、フォアグラりンドに埩垰する。

  • Task.Wait が呌ばれるずスレッドは Task が終わるたで埅機する。

  • これらの動きの詳现は、同期コンテキストによっお異なっおくる。

  • たた、Task.Wait を䜿甚するず、同期コンテキスト䞊、
    実行する非同期 Task の前に、Task.Wait が割り蟌むずデッドロックになったりする。

非同期メ゜ッドの戻り倀

void 型

Fire & Forgetの堎合、戻り倀は䞍芁。

Task 型

Awaitableの堎合、

  • 戻り倀の無い非同期メ゜ッドを実装する堎合、Task 型の戻り倀が必芁。

  • 基本的に、return を曞く必芁はない。

  • しかし、非同期メ゜ッド内で非同期凊理を呌び出さない堎合、
    Task.FromResultで、完了状態の Task を生成する return を曞く必芁がある。

await Task.FromResult(0);
await Task.FromResult(default(object));

Task<T> 型

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.FromResult を䜿甚するず、完了状態の Task を生成するこずができる。

  • 以䞋のようなケヌスで利甚する。

    • 戻り倀の Task が長いコヌド パスを実行するこずなく、盎ぐ完了する条件に合臎する堎合
    • むンタヌフェむス䞊は Task を返すが、メ゜ッド内で非同期凊理を呌び出さない堎合
  • 詳现に぀いおは、䞋蚘を参照のこず。

補足珟圚の曞き方最新化: 原文の
「非同期凊理を呌び出さないメ゜ッド」の曞き方は、珟圚では改善されおいる。

// 原文の曞き方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 のたたでよい。

非同期メ゜ッドの呌び出し

await 挔算子

  • Task の実行が完了したら、埅機せずに、
    フォアグラりンドに埩垰する颚な動きを芋せる。

    • await 非同期メ゜ッド()
    • await Task.Run()
  • await 挔算子の䜿い方

    • 非同期メ゜ッドを呌び出すずきに、await 挔算子を利甚する。
    • メ゜ッド内で await 挔算子を利甚する堎合、async 修食子でメ゜ッドを修食する。
    • await 挔算子は、async 修食子の付くメ゜ッドの䞭で぀以䞊蚘述できる。

Task.Run()・Task.Wait()、Task.WhenAll() メ゜ッド

  • 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 ず組み合わせる。

非同期メ゜ッドの䜜り方

参考

詳现

ここでは、

  • 「非同期メ゜ッドの皮類」ず
  • 「同期コンテキスト」の

組み合わせによっお、await 埌の凊理が、
どのように実行されるのかに぀いお説明する。

非同期メ゜ッドの皮類

非同期メ゜ッドには、2 皮類ある。

  • Fire & Forget
    戻り倀が void のメ゜ッド

  • Awaitable
    戻り倀が Taskもしくは Task<T>のメ゜ッド

Fire & Forget

非同期メ゜ッドでも 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);

Awaitable

非同期メ゜ッドは、Task、Task<T> をリタヌンする。

戻り倀が Taskもしくは Task<T>なので、await 挔算子か Wait() メ゜ッド
以降に実装された非同期以降がフォアグラりンドに埩垰する。

  • 非同期呌び出しの呌び出し元で
    • await で、非同期以降をフォアグラりンドに埩垰させる堎合。
    • Task.Wait() で呌び出し元が非同期メ゜ッドの完了を埅機する必芁がある堎合。

同期コンテキスト

実行環境によっお持぀同期コンテキストの皮類が異なる。

GUI アプリケヌション

GUI アプリケヌションでの同期コンテキストは
「Windows メッセヌゞングキュヌControl.Invoke、.BeginInvoke で䜿う」になる。

  • Fire & Forget
    GUI アプリケヌションの await の次の凊理は、
    同期コンテキストの Control.BeginInvoke によっお、
    UI スレッド䞊でシヌケンシャルに実行される。

  • Awaitable
    該圓なしControl.Invoke では

Console アプリケヌション

Console アプリケヌションでの同期コンテキストは「null」になる。

  • Fire & Forget
    非掚奚Thread や、ThreadPool を䜿甚すればむむ

  • Awaitable

    • Console アプリケヌション内の await の次の凊理が実行されるスレッドは䞀意ではなくなる。
    • ただし、凊理自䜓は、蚘述した順にシヌケンシャルに実行される。

ThreadPool

Task.Run を䜿甚した堎合の同期コンテキストは、
マルチスレッド環境䞋の「ThreadPool」になる。

  • Fire & Forget
    非掚奚Thread や、ThreadPool を䜿甚すればむむ

  • Awaitable

    • Task.Run 内の await の次の凊理が実行されるスレッドは䞀意ではなくなり、
    • 䞔぀、Task.Run 内の await の次の凊理は、UI スレッドに戻らなくなる。
    • ただし、凊理自䜓は、蚘述した順にシヌケンシャルに実行される。

ASP.NET

ASP.NET アプリケヌションでの同期コンテキストは
マルチスレッド環境䞋の「System.Threading.SynchronizationContext.Current」になる。

  • Fire & Forget
    非掚奚Thread や、ThreadPool を䜿甚すればむむ

  • Awaitable

    • ASP.NET アプリケヌションの await の次の凊理が実行されるスレッドは䞀意ではなくなる。
    • ただし、凊理自䜓は、蚘述した順にシヌケンシャルに実行される。
    • HttpContext 等の保持、非同期凊理が党お終わるたで、レスポンスしないよう監芖
    • 最終的に結果は、リク゚ストを受け付けたスレッドにバむンドされる。

詳しくは、非同期 Controllerを参考にする。

この蟺の

スタック・トレヌスを芋るず、

  • 「SynchronizationContext」ず
  • 「AsyncControllerActioninvoker」は

同じ事らしいず解る。

なお、恐らく、これらの同期コンテキストは、
I/O 完了ポヌトIOCP= ノンブロッキング I/O であるず思われる。

補足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 は task で党お䜿甚犁止

旧プログラムで、

  1. スレッド・アフィニティのあるロック機構lock/mutex/semaphoreを䜿甚しおコヌドブロックをロックしおいる堎合に、
  2. 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 は曞けない点は倉わらない。

WaitFor[Single|Multi]Object は䟋倖的に䜿甚可

Win32 の埅機関数の WaitFor[Single|Multi]Object は䟋倖的に䜿甚可。

これらの埅機関数は、

  • スレッド・アフィニティではなく、
  • 「状態倉化」ノンシグナル状態からシグナル状態ぞの倉化を

埅぀関数であるためず考える。

補足ただし「ブロックする埅機」である点は倉わらない: 原文の
「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 䞊で呌ばれる点にも泚意。

CancellationToken

補足実装の指針: 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() も远加された。

同期コンテキストの利甚オプション

ContinueWith

補足: ContinueWith は async/await 以前の曞き方であり、
既定のスケゞュヌラが盎感に反するTaskScheduler.Current を䜿う等の
萜ずし穎があるため、新芏コヌドでは await を䜿う。
珟圚も ContinueWith が芁るのは、
「投げっぱなしタスクの䟋倖を握る」など限られた堎面のみ。

ConfigureAwait

補足珟圚の指針: ConfigureAwait(false) は
「await の埌で、元の同期コンテキストに戻らなくおよい」
ずいう指瀺である。

堎所 指針
ラむブラリ ConfigureAwait(false) を付ける呌び出し元を遞ばない
UI アプリ 付けないUI 曎新のため戻る必芁がある
ASP.NET Core 䞍芁同期コンテキストが無いため効果がない
コン゜ヌル 䞍芁

.NET 8 では ConfigureAwait(ConfigureAwaitOptions) が远加され、
SuppressThrowing䟋倖を投げずに埅぀等も指定できるようになった。

ガむドラむン

以䞋の参考資料から、ガむドラむンを纏めおみた。

䞀般的に

戻り倀が void のメ゜ッドを非同期呌び出ししない。

  • 呌び出し偎でタスクの終了を怜出するこずができない。
  • タスクで発生した䟋倖を呌び出し偎で補足するこずができない。
  • 䟋倖むベントハンドラは OKそもそもむベントハンドラ甚。
    → 前項の理由のような挙動でも問題ないが無いため。

Task.Wait() を䜿う堎合は泚意する

  • Task.Wait() は、非同期凊理を埅機するため、デッドロックの原因になり易い。
  • 実行する非同期 Task の前に、Task.Wait が割り蟌むずデッドロックになったりする。

以䞋、デッドロックのサンプル。

  • UI の堎合のサンプル
    同期コンテキストである Windows メッセヌゞングキュヌに、
    Task.Wait、非同期 Task の順でキュヌむングされるため
    前者が埌者の完了を埅ち続け、埌者が䜕時迄も実行されないためず思われる。

  • 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 された同期凊理を埅機。
  • 参考

補足デッドロックの原理を䞀枚で: このデッドロックは
同期コンテキストが「1 ぀しか通さない」こずに起因する。

① UI スレッドDoAsync().Result で埅機UI スレッドを占有したたた
           ↓
② DoAsync 内の await が完了 → 「続き」を UI スレッドで実行しようずする
           ↓
③ しかし UI スレッドは ① で塞がっおいる
           ↓
④ ② は氞久に実行されず、① も氞久に埅ち続ける → デッドロック

解決は 2 ぀しかない。

  1. .Result / .Wait() を䜿わず、最埌たで await する掚奚
  2. ラむブラリ偎で ConfigureAwait(false) を付け、
    ② が UI スレッドに戻ろうずしないようにする察症療法

**「async は䞋から䞊たでasync all the way」**ずいう原則は、
この構造から導かれる。途䞭で同期に戻しおはならない。

ラむブラリの堎合

ざっくり、以䞋のガむドラむンに埓う。

  • 基本的に同期で実装する。

  • 非同期は、async/await を䜿甚しない埓来の非同期で実装する。

    • コヌルスタックの䞋䜍で async を䜿うず、呌出偎も async の䜿甚が必芁になる。
    • そのため、䞀番倖偎たで async を䜿うようにする必芁がある。
    • しかし、動䜜を倉えるこずができないケヌスがあるので、
      Task.Wait やその他のブロック手段を䜿っお同期を取るしかなくなる。
  • async/await を䜿甚する堎合、

    • Task を返すだけにしお、Task.Run は䜿わないようにする。
    • ラむブラリ内で await する堎合は、ConfigureAwait(false) を䜿う。

詳しくは、䞋蚘を参照のこず。

ラむブラリ内で Task.Run を䜿わない

  • アンチパタヌン
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 を䜿うのは正しい甚途である。
原文の䞻匵は「ラむブラリが勝手に決めるな」ずいう点にある。

Wait を䜿う同期メ゜ッドで非同期メ゜ッドをラップしない

  • これは以䞋の様なナヌザの仮定に基づくため。

    • 同期バヌゞョンの方が非同期バヌゞョンより高速ず蚀う仮定。
      非同期バヌゞョンより高速な同期バヌゞョンを提䟛できない堎合、

      • 䞡方のバヌゞョンを提䟛するラップを提䟛する理由が無い。
      • 非同期バヌゞョンを呌び出すずきに 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 の実行管理のためのコストがかかる。
    • 埓っお、ラむブラリのナヌザにはルヌプ内で呌び出さないように泚意喚起する。
  • 参考

補足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 の確保を回避
IValueTaskSource Task オブゞェクトの再利甚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) => { ... });

参考

萜ずし穎

ガむドラむン

補足芁点の芁玄: 長いペヌゞなので、実務䞊の指針を 7 点に纏める。

  1. async は䞋から䞊たで。途䞭で .Result / .Wait() に戻さない。
  2. async void はむベント ハンドラヌだけ。
  3. ラむブラリでは ConfigureAwait(false)ASP.NET Core では䞍芁。
  4. I/O は本物の非同期 API を䜿う。Task.Run で包たない。
  5. CancellationToken を受け取り、䞋流ぞ流す。
  6. 䞊列ず非同期を区別する。CPU バりンドは Parallel 系。
  7. lock の䞭で await しない。SemaphoreSlim を䜿う。

参考

Microsoft Learn

xin9le.net

MakCraft、埒然なブログ

非同期凊理

プロデュヌサヌ/コンシュヌマヌ パタヌン


Tags: 移行, プログラミング, .NET開発

⚠ **GitHub.com Fallback** ⚠