Diese Präsentation wurde erfolgreich gemeldet.
Wir verwenden Ihre LinkedIn Profilangaben und Informationen zu Ihren Aktivitäten, um Anzeigen zu personalisieren und Ihnen relevantere Inhalte anzuzeigen. Sie können Ihre Anzeigeneinstellungen jederzeit ändern.

async/await のしくみ

14.180 Aufrufe

Veröffentlicht am

https://connpass.com/event/95696/
2018/9/15 「Unity 非同期完全に理解した勉強会」にて登壇

Veröffentlicht in: Technologie
  • Als Erste(r) kommentieren

async/await のしくみ

  1. 1. async/await のしくみ 岩永 信之
  2. 2. 前振り: C#の歴史 1.0 2.0 3.0 4.0 5.0 6.0 7.0 初期リリース ジェネリクス イテレーター LINQ dynamic 相互運用 async/await Compiler Platform (Roslyn) タプル パターンマッチ Unity 5.X 以前 Unity 2017/2018 Incremental Compiler 本日のテーマ ちなみに、 C# 7.2まで行ける
  3. 3. await演算子: 登場以前 • 元(async/await以前) • コールバック地獄 var c = new HttpClient(); c.GetAsync("http://ufcpp.net").ContinueWith(tr => { var res = tr.Result; res.Content.ReadAsStringAsync().ContinueWith(tc => { var content = tc.Result; Console.WriteLine(content); }); }); もっと面倒なことも • 例外処理は? • 分岐やループは?
  4. 4. await演算子: 登場後 • C# 5.0以降 • 非同期なところにawait演算子を足すだけ • 例外処理 → try-catchが普通に使える • 分岐やループ → if, for, while, foreachが普通に使える var c = new HttpClient(); var res = await c.GetAsync("http://ufcpp.net"); var content = await res.Content.ReadAsStringAsync(); Console.WriteLine(content);
  5. 5. 今日の話 • ほんとに非同期なの? • ちゃんとコールバック呼び出しに展開されてる • オーバーヘッド大きくない? • ある程度やっぱある • 用途には注意 • Rxもあるけど? • 単一の値待ちにはawaitを • UniRxにもAwaiterあるよ この辺りのことを 中身を追いつつ説明
  6. 6. 非同期処理 そもそも、非同期って?
  7. 7. 非同期もいろいろ • 同時実行 (concurrency) • 並列実行 (parallelism) • I/O待ち (I/O completion)
  8. 8. 同時実行 (concurrency) • CPUをシェア • 誰かが重たいことやってても、他のみんなに迷惑かけない • OSの強権(preemptive)で処理を奪い取って切り替え スレッド1 スレッド2 • 一定時間ごとにスレッドを切り替え • 1人の独占を許さない(OSが強制) • 例え単一CPUコアでも同時に複数の処理が動いてる
  9. 9. 同時実行とUIスレッド • GUIアプリには「UIスレッド」がある • ユーザーからの入力を受け付けているスレッド • このスレッドを止めるとフリーズ感がひどい UIスレッド その他のスレッド UIスレッドが空いているときだけ ユーザー入力を受付可能 ユーザー入力 UIスレッド上で 重たい処理をすると フリーズを起こす
  10. 10. 並列実行 (parallelism) • 複数のCPUをフルにぶん回したい • それぞれのCPUで別スレッドを実行 • 上手く使えれば、N個のCPUでN倍の処理ができる • (複数のスレッドで同じデータを共有すると上手くいかない) スレッド1 スレッド2 … • 複数のCPUで処理を振り分け
  11. 11. I/O待ち (I/O Completion) • CPUの外の世界とのやり取り • CPUと比べて、外の世界は数ケタ以上遅い • 遅いものを待っている間、他の処理をしないのはもったいない • 使いもしないリソースを握りっぱなしにするのはもったいない メイン メモリ (2~3桁遅い) シリコン ドライブ (3~4桁遅い) HDD (5~6桁遅い) インターネット (物理的に近くても7桁くらい遅い) CPUの 外の世界 リソースを解放 コールバック
  12. 12. awaitはどれ?(1) • awaitが一番活躍するのは非同期I/O待ち var c = new HttpClient(); var res = await c.GetAsync("http://ufcpp.net"); var content = await res.Content.ReadAsStringAsync(); Console.WriteLine(content); コールバック リソースを解放 リソースを 解放
  13. 13. awaitはどれ?(2) • UIスレッドを止めないのにも有効 async void OnClick(object sender, EventArgs args) { // ユーザー入力を受け取り await Task.Run(重たい処理); // 結果を UI に表示 } UIスレッド その他のスレッド 重たい処理はUIス レッドの外でやら ないとフリーズ Runの中身は別のスレッド で実行される UIスレッドに戻す仕組み(同期コンテキスト)については後述
  14. 14. 標準ライブラリの 非同期処理機能 C#で非同期処理するのにどんなクラスを使うか
  15. 15. .NETの非同期処理がらみ • Thread (生のスレッド) • ThreadPool (スレッドの再利用) • その上にいろいろ • Task • UniTaskとかもこのレイヤー
  16. 16. Thread (System.Threading)クラス • 「同時実行」で話したスレッドそのものを表すクラス • OSが強権を持って切り替えを保証 • その代わりだいぶ重たい スレッド1 スレッド2 これ var t = new Thread(() => { // 新しいスレッドで実行したい処理 }); t.Start();
  17. 17. スレッドの負担 • スレッドが消費するリソース(1スレッド辺り) • スレッド自体の管理情報(1KB※) • スタック メモリ(1MB※) • スレッド開始・終了時のコスト • OSのイベントが飛ぶ • スレッド切り替えに伴うコスト • OSの特権モードへの移行・復帰 • レジスターの保存・復元 • 次に実行するスレッドの決定 ※ Windowsの場合 どれもそれなりに 性能へのインパクト大きい Threadクラスを生で使うことは ほとんどない
  18. 18. スレッドプール • 事前にいくつかスレッドを立てておいて、使いまわす仕組み • スレッドに係る負担を削減 • ただし、優先度とか実行順とかの細かい保証はできない スレッド プール キュー タスク1 タスク2 … 数本のスレッドだけ用意 (足りなくなったら増やす) 空いているスレッドを探して実行 (長時間空かない時だけ新規スレッド作成) 新規タスク タスクは一度 キューに溜める
  19. 19. I/O待ちとスレッドプール • 非同期I/O API • WindowsだとI/O完了ポートっていうAPIを利用 • (Linuxだとepoll、BSD/Macだとkqueue) • I/Oが完了したらスレッドプールにコールバック処理を投函する作り var bytes = await File.ReadAllBytesAsync("file name"); スレッド プール キュー I/O完了ポート I/O開始 I/O完了 タスク1
  20. 20. ThreadPool (System.Threading)クラス • 名前通り、スレッド プールを使うためのクラス • .NET 3.5時代まではよく使った • まだ使いづらい点がある • 非同期処理の完了を待って違う非同期処理を開始したいとき • 特に、例外や、処理の結果得られる値を使いたいとき ThreadPool.QueueUserWorkItem(_ => { // スレッド プール上で実行したい処理 }); // ここに何か書くと、↑とは同時実行になる(完了を待てない)
  21. 21. Task (System.Threading.Tasks)クラス • 非同期処理の「続き」が書けるクラス • 新規に非同期処理を始める時はRun • 非同期処理のあとに何か続けたいときはContinueWith Task.Run(() => { // 非同期処理 return 戻り値; }).ContinueWith(t => { var result = t.Result; // 続きの処理 }); 特に指定がない場合※、 スレッドプール上で実行される ※ 指定が必要ならTaskSchedulerと言うものを渡す
  22. 22. Taskクラスとawait • ここで冒頭の方で話したコールバック地獄の話に • ちなみに、Taskクラス以外に対してもawaitできる • この後、その仕組みについての話を Task.Run(() => { // 非同期処理 return 戻り値; }).ContinueWith(t => { var result = t.Result; // 続きの処理 }); var result = await Task.Run(() => { // 非同期処理 return 戻り値; }); // 続きの処理
  23. 23. awaitの中身 サンプル: https://github.com/ufcpp/UfcppSample/tree/master/Demo/2018/AsyncInternal
  24. 24. 例として: ネットとかから一覧取得 • 同期処理で良ければ // インデックスをどこかから取って来て var indexes = GetIndex(); // その中の一部分を選んで var selectedIndexes = SelectIndex(indexes); // 選んだものの中身を取得 var contents = new List<string>(); foreach (var index in selectedIndexes) { var content = GetContent(index); contents.Add(content); } ネットから取ってきたり ファイルから読むにしてもI/O ユーザーに選択してもらったり ユーザー入力もI/O 同じくネットから で、これを非同期にしたい
  25. 25. コールバック型の非同期だと // インデックスをどこかから取って来て GetIndex(indexes => { // その中の一部分を選んで SelectIndex(indexes, selectedIndexes => { // 選んだものの中身を取得 var contents = new List<string>(); var e = selectedIndexes.GetEnumerator(); Action<string> getContentCallback = null; void next() { if (e.MoveNext()) GetContent(e.Current, getContentCallback); else callback(contents); } getContentCallback = content => { contents.Add(content); next(); }; next(); }); }); ネストが 深くなる フォントを12ptに変えないと 入らない程度に長くなる foreachを手動で展開
  26. 26. async/awaitなら • awaitを使って書き直し // インデックスをどこかから取って来て var indexes = await GetIndex(); // その中の一部分を選んで var selectedIndexes = await SelectIndex(indexes); // 選んだものの中身を取得 var contents = new List<string>(); foreach (var index in selectedIndexes) { var content = await GetContent(index); contents.Add(content); } (2ページ前の同期コードと比べて) 非同期処理なところにawaitを足すだけ 処理フローは同期の場合とまったく同じ
  27. 27. async/awaitの中身 • 同期っぽいコードから非同期コードを機械的に生成してる • 生成しているのはコールバック型のコード • 次のスライドから生成手順を紹介 • 何段階かにわけて説明 • 段階ごとにコミットした例: https://github.com/ufcpp/UfcppSample/commits/85f7901c19264b2b9b1547a87ef2 d80bb49b076d/
  28. 28. 段階1: クラス生成 • ただのメソッドからクラスを生成する • (このデモでは匿名関数で代用※) void anonymous() => { var indexes = GetIndex(); var selectedIndexes = SelectIndex(indexes); var contents = new List<string>(); foreach (var index in selectedIndexes) { var content = GetContent(index); contents.Add(content); } }; ※ プレゼン都合(匿名関数の方がコードが短い)。匿名関数も内部的にはクラスが生成される
  29. 29. 段階2: 中断・再開コード • 状態の記録とgoto用ラベル挿入 var indexes = await GetIndex(); state = 1; tIndexes = GetIndex(); if (!tIndexes.IsCompleted) { tIndexes.ContinueWith(_ => anonymous()); return; } Case1: var indexes = tIndexes.Result; tIndexes = default; どこまで実行したかを記録して 再開時にはここにgoto 完了済みの場合は素通りして 未完了ならContinueWithで 自分自身をコールバック登録 結果を受け取って 一時変数を後片付け (ほんとは ContinueWithだけじゃなく SynchronizationContext.Postも)
  30. 30. この時点で言えること • 非同期メソッドは必ずしも非同期じゃない • 1個目のawaitよりも手前までは普通に同期実行してる • 完了済みタスクをawaitしてもコストは低い • イテレーター(yield return)に似てる • どこまで実行したかの記録と、再開時のgoto • Unityのコルーチンと源流は同じ • イテレーターそのもので非同期処理しなかったのは: • 戻り値があるとしんどい • 例外処理しんどい
  31. 31. 段階3: Awaitableパターン化 • Task以外の型もawaitできるように(Awaitableパターン) var indexes = await GetIndex(); state = 1; tIndexes = GetIndex().GetAwaiter(); if (!tIndexes.IsCompleted) { tIndexes.OnCompleted(() => anonymous()); return; } Case1: var indexes = tIndexes.GetResult(); tIndexes = default; そのものではなく、所定のパターンを 満たすAwaiterと呼ばれる型を介する OnCompletedメソッドと GetResultメソッドを 持っていればどんな型でもOK
  32. 32. awaitableに求められる要件 • awaitable (await演算子を使える型) • awaiter GetAwaiter() • awaiter • bool IsCompleted, T GetResult() • INotifyCompletion.OnCompleted(Action) • ICriticalNotifyCompletion.UnsafeOnCompleted(Action) 一段別の型を挟む(拡張できるように)
  33. 33. awaiterの例 • 継続呼び出しの仕方をカスタマイズ Task t; await t; Task t; await t.ConfigureAwait(false); Task.GetAwaiter TaskAwaiterクラス※ var s = SynchronizationContext.Current; t.ContinueWith(t1 => { s.Post(_ => continuation(), null); }); ConfiguredTaskAwaitable.GetAwaiter ConfiguredTaskAwaiterクラス※ t.ContinueWith(t1 => { continuation(); }); ※ この通りのコードになっているわけではなく、似たような挙動になるコードを例示 同期コンテキスト (次項で説明)
  34. 34. UIスレッド(メイン スレッド) • UI関連の処理は単一のスレッドで行われている(UIスレッド) UIスレッド その他のスレッド ユーザーからの入力イベント はUIスレッド上で処理される 描画APIはUIスレッド上 でだけ呼べる
  35. 35. 同期コンテキスト • SynchronizationContextクラス(System.Threading) UIスレッド その他のスレッド 入力 イベント Task.Run SynchronizationContext .Post 重たい処理 描画 所望のスレッドに戻ってくるため に使うのが同期コンテキスト
  36. 36. 同期コンテキストの使い方 1. Currentで現在のスレッドのコンテキスト(文脈)を取得 2. Postでそのコンテキストに処理を戻す // ユーザー入力を受け取り await Task.Run(重たい処理); // 結果を UI に表示 // ユーザー入力を受け取り var sc = SynchronizationContext.Current; Task.Run(重たい処理).ContinueWith(t => { sc.Post(_ => { // 結果を UI に表示 }, null); }); TaskAwaiterはこの処理を 内部で自動的にやってる
  37. 37. 段階4: ローカル変数をフィールドに • awaitをまたぐためにはローカル変数ではダメ • 継続呼び出し時に値を保持できるように、フィールドに変更する • (このデモでは匿名関数+変数キャプチャで代用※) void anonymous() => { ... var indexes = tIndexes.GetResult(); tIndexes = default; ... } List<string> indexes; void anonymous() => { ... indexes = tIndexes.GetResult(); tIndexes = default; ... } ※ 匿名関数で変数をキャプチャすると、内部的にはフィールドが生成される
  38. 38. 段階5: 戻り値 • TaskCompletionSourceに置き換え void anonymous() => { ... return contents; } var r = new TaskCompletionSource<IEnumerable<string>>() void anonymous() => { ... r.SetResult(contents); } return r.Task;
  39. 39. 段階6: ビルダー パターン化 • 戻り値や、OnCompleted呼び出しの部分もパターン化 (非同期メソッド ビルダー パターン) • Task以外の戻り値を返せるようにする var r = new TaskCompletionSource<...>() ... { ... tIndexes.OnCompleted(...); ... r.SetResult(contents); } return r.Task; var builder = new AsyncTaskMethodBuilder<...>() ... { ... builder.AwaitOnCompleted(ref tIndexes, ref this); ... builder.SetResult(contents); } return builder.Task; (MethodBuilderに求められる要件は複雑なので割愛)
  40. 40. Task以外の戻り値の例 • ValueTask構造体(System.Threading.Tasks名前空間) • 同期的に完了している場合は値をそのまま保持 • 真に非同期が必要な場合だけTaskクラスを作る async Task<int> XAsync() { if (_9割方true()) return 1; await Task.Delay(1); return 0; } 9割方、同期にもかかわらず 常にTaskクラスがnewされる async ValueTask<int> XAsync() { if (_9割方true()) return 1; await Task.Delay(1); return 0; } • 戻り値をValueTaskに変えるとTaskのnewがなくなる • 内部ではAsyncValueTaskMethodBuilderが使われる
  41. 41. まとめ • await演算子 • 同期処理と同じ書き方で非同期処理できる • 中断・再開コードを生成してる • 最初のawaitまでは実は同期処理 • 真に非同期処理を必要としている場合だけContinueWith • パターン化 • awaitableパターン: Task以外をawaitできる • 非同期メソッド ビルダー パターン: Task以外を戻り値にできる

×