Upgrade to Pro — share decks privately, control downloads, hide ads and more …

C# async/await 勉強会

C# async/await 勉強会

C# の非同期処理における async/await の構造や動作原理についての解説。
(2023/1/27 社内勉強会用)

Avatar for Hiroki Kamiyoshikawa

Hiroki Kamiyoshikawa

May 05, 2025
Tweet

More Decks by Hiroki Kamiyoshikawa

Other Decks in Programming

Transcript

  1. async/await の登場 var foo = new Foo(); foo.Completed += result

    => { Debug.Log($" 答えは {result} です"); }; foo.Do(); public class Foo { public event Action<int> Completed; public void Do() { new Thread(() => { // とても重い計算処理 var result = 1 + 1; Completed?.Invoke(result); }).Start(); } } var foo = new Foo(); var result = await foo.DoAsync(); Debug.Log($" 答えは {result} です"); public class Foo { public async UniTask<int> DoAsync() { await UniTask.SwitchToThreadPool(); // とても重い計算処理 return 1 + 1; } } 完了イベントによるコールバック async/await による実装 多段に非同期処理を書くと可読性が悪い 非同期処理内の例外のハンドリングが困難 await を並べるだけで多段に非同期実行可能 非同期処理内の例外も呼び出し元まで伝播する
  2. 非同期処理とバックグラウンド処理 バックグラウンドスレッド実行のために非同期処理を簡潔に書くための構文が必要 GUI アプリケーションでは重い処理をバックグラウンドスレッドで実行したい Task, async/await 構文 async/await 登場から UniTask

    登場まで UniTask Unity バックグラウンド実行することを主目的として async/await と Task を導入したため、メインスレッド内のみで非同期処理をする API は Task には用意されていない ・スレッドの概念と非同期処理の概念を分離している ・Unity で使える各種便利機能 Unity 以外ではバックグラウンド処理以外で非同期処理を扱う必要がほぼないため、 決して Task と async/await の作成者が 非同期とスレッドの概念を混同しているわけではない。
  3. Time Frame n n+1 n+2 n+a EarlyUpdate Update LateUpdate object1

    object2 object3 object1 object2 object3 object1 object2 object3 CPU の処理の流れ プログラマが実装したい処理の流れ CPU の処理の流れとプログラマが実装したい処理の流れの乖離 Unity Player Loop 両者の流れが違うため、実装が複雑になる
  4. Time Frame n n+1 n+2 n+a EarlyUpdate Update LateUpdate object1

    object2 object3 object1 object2 object3 object1 object2 object3 'await' = その位置で CPU の処理の流れをジャンプする ( かもしれない) 'async' = そのメソッドの中でジャンプする ( かもしれない) ことを示すマーク await UniTask.DelayFrame(a) await UniTask.Yield(PlayerLoopTiming.EarlyUpdate) await UniTask.CompletedTask ( ジャンプしない) async と await の意味 await によってプログラマにとって都合のいい処理の流れを作れる Unity Player Loop
  5. Time Frame n n+1 n+2 n+a EarlyUpdate Update LateUpdate object1

    object2 object3 await はスレッド間をジャンプすることもできる Background Thread Main Thread await UniTask.SwitchToThreadPool() await UniTask.SwitchToMainThread(PlayerLoopTiming.LateUpdate) object1 object2 object3 await によるスレッド間移動 Unity Player Loop
  6. 同期コンテキスト (SynchronizationContext) await UniTask.SwitchToThreadPool() await UniTask.Yield() await UniTask.CompletedTask ( ジャンプしない)

    await Task.Run(action) ( メインスレッドで開始) action Main Thread Thread A await Task.Run(action) ( 非メインスレッドで開始) action Thread A Thread B await Task.Run(action).ConfigureAwait(false) ( メインスレッドで開始) action Main Thread Thread A Main Thread Thread A await UniTask.DelayFrame(a) メインスレッドの同期コンテキストの作用で 暗黙的にメインスレッドに戻る action Thread A Thread B await Task.Run(action).ConfigureAwait(false) ( 非メインスレッドで開始) 明示的に同期コンテキストをつかまないよう指示 (.ConfigureAwait(false)) 非メインスレッド (Thread A) には 同期コンテキストは存在しないため Thread B で継続 同期コンテキストは存在しないため ConfigureAwait の指定は true でも false でも 戻らない UniTask Task UniTask は同期コンテキストをキャプチャしない。 終了時のスレッドでそのまま継続処理を実行。 Task は同期コンテキストがあればキャプチャする。 キャプチャすると元のスレッドに戻り継続処理を実行。
  7. async メソッドの戻り値 await Hoge(true) DoSomething() Main Thread Thread A async

    UniTask Hoge(bool xxx) { if (xxx) { return; } await UniTask.SwitchToThreadPool(); DoSomething(); return; } Main Thread await Hoge(false) await Hoge(true) DoSomething() Main Thread Thread A async Task Hoge(bool xxx) { if (xxx) { return; } await UniTask.SwitchToThreadPool(); DoSomething(); return; } Main Thread await Hoge(false) async Task はメインスレッドで await すると 暗黙的にメインスレッドに戻る ( 同期コンテキストをキャプチャする) async UniTask は終了時のスレッドで継続する ( 同期コンテキストをキャプチャしない) async メソッドはコンパイラによって async を使わない形に変形される。async は糖衣構文。 その戻り値の UniTask, Task はコンパイラが自動生成する。 UniTask Task
  8. await によるジャンプはデリゲートのキュー UniTask Task Main Thread Thread A async UniTask

    Piyo() { await UniTask.SwitchToThreadPool(); DoSomething(); return; } await Piyo() Piyo() DoSomething() UniTask.SwitchToThreadPool() Thread A のキューに DoSomething を入れる キューから DoSomething が とりだされて実行 Piyo() の戻り値は UniTask であるため メインスレッドの同期コンテキストを キャプチャしていない。 そのまま継続処理を実行 Main Thread Thread A Piyo() DoSomething() UniTask.SwitchToThreadPool() UniTask と同じ Piyo() の戻り値は Task であるため メインスレッドの同期コンテキストを キャプチャしている。 同期コンテキストのキューに 継続処理をポスト UniTask と同じ 同期コンテキストのキューから 継続処理を取り出して実行 await Piyo() async Task Piyo() { await UniTask.SwitchToThreadPool(); DoSomething(); return; }
  9. async void はなぜ非推奨か Main Thread Thread A Main Thread Thread

    A 例外発生 catch Main Thread Thread A await による待機呼び出し await による呼び出しは どこで例外が発生しても await を追跡して遡る Main Thread Thread A Main Thread Thread A 例外発生 catch async void による待機なし呼び出し async void は await できないため ジャンプした処理を追跡できない Main Thread Thread A 未キャッチ例外が漏れる async void は開始した処理の 終了を検知できないため 継続処理を書けない 同期的な例外はキャッチ可能 async void を await できないのは何が困るのか async void を書いてもいい条件 ・未キャッチ例外が確実に漏れないこと ・開始した非同期処理の終了を検知する必要がないこと (fire and forget)