Advertisement

More Related Content

Slideshows for you(20)

Advertisement
Advertisement

async/await のしくみ

  1. async/await のしくみ 岩永 信之
  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. 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. 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. 今日の話 • ほんとに非同期なの? • ちゃんとコールバック呼び出しに展開されてる • オーバーヘッド大きくない? • ある程度やっぱある • 用途には注意 • Rxもあるけど? • 単一の値待ちにはawaitを • UniRxにもAwaiterあるよ この辺りのことを 中身を追いつつ説明
  6. 非同期処理 そもそも、非同期って?
  7. 非同期もいろいろ • 同時実行 (concurrency) • 並列実行 (parallelism) • I/O待ち (I/O completion)
  8. 同時実行 (concurrency) • CPUをシェア • 誰かが重たいことやってても、他のみんなに迷惑かけない • OSの強権(preemptive)で処理を奪い取って切り替え スレッド1 スレッド2 • 一定時間ごとにスレッドを切り替え • 1人の独占を許さない(OSが強制) • 例え単一CPUコアでも同時に複数の処理が動いてる
  9. 同時実行とUIスレッド • GUIアプリには「UIスレッド」がある • ユーザーからの入力を受け付けているスレッド • このスレッドを止めるとフリーズ感がひどい UIスレッド その他のスレッド UIスレッドが空いているときだけ ユーザー入力を受付可能 ユーザー入力 UIスレッド上で 重たい処理をすると フリーズを起こす
  10. 並列実行 (parallelism) • 複数のCPUをフルにぶん回したい • それぞれのCPUで別スレッドを実行 • 上手く使えれば、N個のCPUでN倍の処理ができる • (複数のスレッドで同じデータを共有すると上手くいかない) スレッド1 スレッド2 … • 複数のCPUで処理を振り分け
  11. I/O待ち (I/O Completion) • CPUの外の世界とのやり取り • CPUと比べて、外の世界は数ケタ以上遅い • 遅いものを待っている間、他の処理をしないのはもったいない • 使いもしないリソースを握りっぱなしにするのはもったいない メイン メモリ (2~3桁遅い) シリコン ドライブ (3~4桁遅い) HDD (5~6桁遅い) インターネット (物理的に近くても7桁くらい遅い) CPUの 外の世界 リソースを解放 コールバック
  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. awaitはどれ?(2) • UIスレッドを止めないのにも有効 async void OnClick(object sender, EventArgs args) { // ユーザー入力を受け取り await Task.Run(重たい処理); // 結果を UI に表示 } UIスレッド その他のスレッド 重たい処理はUIス レッドの外でやら ないとフリーズ Runの中身は別のスレッド で実行される UIスレッドに戻す仕組み(同期コンテキスト)については後述
  14. 標準ライブラリの 非同期処理機能 C#で非同期処理するのにどんなクラスを使うか
  15. .NETの非同期処理がらみ • Thread (生のスレッド) • ThreadPool (スレッドの再利用) • その上にいろいろ • Task • UniTaskとかもこのレイヤー
  16. Thread (System.Threading)クラス • 「同時実行」で話したスレッドそのものを表すクラス • OSが強権を持って切り替えを保証 • その代わりだいぶ重たい スレッド1 スレッド2 これ var t = new Thread(() => { // 新しいスレッドで実行したい処理 }); t.Start();
  17. スレッドの負担 • スレッドが消費するリソース(1スレッド辺り) • スレッド自体の管理情報(1KB※) • スタック メモリ(1MB※) • スレッド開始・終了時のコスト • OSのイベントが飛ぶ • スレッド切り替えに伴うコスト • OSの特権モードへの移行・復帰 • レジスターの保存・復元 • 次に実行するスレッドの決定 ※ Windowsの場合 どれもそれなりに 性能へのインパクト大きい Threadクラスを生で使うことは ほとんどない
  18. スレッドプール • 事前にいくつかスレッドを立てておいて、使いまわす仕組み • スレッドに係る負担を削減 • ただし、優先度とか実行順とかの細かい保証はできない スレッド プール キュー タスク1 タスク2 … 数本のスレッドだけ用意 (足りなくなったら増やす) 空いているスレッドを探して実行 (長時間空かない時だけ新規スレッド作成) 新規タスク タスクは一度 キューに溜める
  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. ThreadPool (System.Threading)クラス • 名前通り、スレッド プールを使うためのクラス • .NET 3.5時代まではよく使った • まだ使いづらい点がある • 非同期処理の完了を待って違う非同期処理を開始したいとき • 特に、例外や、処理の結果得られる値を使いたいとき ThreadPool.QueueUserWorkItem(_ => { // スレッド プール上で実行したい処理 }); // ここに何か書くと、↑とは同時実行になる(完了を待てない)
  21. Task (System.Threading.Tasks)クラス • 非同期処理の「続き」が書けるクラス • 新規に非同期処理を始める時はRun • 非同期処理のあとに何か続けたいときはContinueWith Task.Run(() => { // 非同期処理 return 戻り値; }).ContinueWith(t => { var result = t.Result; // 続きの処理 }); 特に指定がない場合※、 スレッドプール上で実行される ※ 指定が必要ならTaskSchedulerと言うものを渡す
  22. Taskクラスとawait • ここで冒頭の方で話したコールバック地獄の話に • ちなみに、Taskクラス以外に対してもawaitできる • この後、その仕組みについての話を Task.Run(() => { // 非同期処理 return 戻り値; }).ContinueWith(t => { var result = t.Result; // 続きの処理 }); var result = await Task.Run(() => { // 非同期処理 return 戻り値; }); // 続きの処理
  23. awaitの中身 サンプル: https://github.com/ufcpp/UfcppSample/tree/master/Demo/2018/AsyncInternal
  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. コールバック型の非同期だと // インデックスをどこかから取って来て 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. 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. async/awaitの中身 • 同期っぽいコードから非同期コードを機械的に生成してる • 生成しているのはコールバック型のコード • 次のスライドから生成手順を紹介 • 何段階かにわけて説明 • 段階ごとにコミットした例: https://github.com/ufcpp/UfcppSample/commits/85f7901c19264b2b9b1547a87ef2 d80bb49b076d/
  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. 段階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. この時点で言えること • 非同期メソッドは必ずしも非同期じゃない • 1個目のawaitよりも手前までは普通に同期実行してる • 完了済みタスクをawaitしてもコストは低い • イテレーター(yield return)に似てる • どこまで実行したかの記録と、再開時のgoto • Unityのコルーチンと源流は同じ • イテレーターそのもので非同期処理しなかったのは: • 戻り値があるとしんどい • 例外処理しんどい
  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. awaitableに求められる要件 • awaitable (await演算子を使える型) • awaiter GetAwaiter() • awaiter • bool IsCompleted, T GetResult() • INotifyCompletion.OnCompleted(Action) • ICriticalNotifyCompletion.UnsafeOnCompleted(Action) 一段別の型を挟む(拡張できるように)
  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. UIスレッド(メイン スレッド) • UI関連の処理は単一のスレッドで行われている(UIスレッド) UIスレッド その他のスレッド ユーザーからの入力イベント はUIスレッド上で処理される 描画APIはUIスレッド上 でだけ呼べる
  35. 同期コンテキスト • SynchronizationContextクラス(System.Threading) UIスレッド その他のスレッド 入力 イベント Task.Run SynchronizationContext .Post 重たい処理 描画 所望のスレッドに戻ってくるため に使うのが同期コンテキスト
  36. 同期コンテキストの使い方 1. Currentで現在のスレッドのコンテキスト(文脈)を取得 2. Postでそのコンテキストに処理を戻す // ユーザー入力を受け取り await Task.Run(重たい処理); // 結果を UI に表示 // ユーザー入力を受け取り var sc = SynchronizationContext.Current; Task.Run(重たい処理).ContinueWith(t => { sc.Post(_ => { // 結果を UI に表示 }, null); }); TaskAwaiterはこの処理を 内部で自動的にやってる
  37. 段階4: ローカル変数をフィールドに • awaitをまたぐためにはローカル変数ではダメ • 継続呼び出し時に値を保持できるように、フィールドに変更する • (このデモでは匿名関数+変数キャプチャで代用※) void anonymous() => { ... var indexes = tIndexes.GetResult(); tIndexes = default; ... } List<string> indexes; void anonymous() => { ... indexes = tIndexes.GetResult(); tIndexes = default; ... } ※ 匿名関数で変数をキャプチャすると、内部的にはフィールドが生成される
  38. 段階5: 戻り値 • TaskCompletionSourceに置き換え void anonymous() => { ... return contents; } var r = new TaskCompletionSource<IEnumerable<string>>() void anonymous() => { ... r.SetResult(contents); } return r.Task;
  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. 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. まとめ • await演算子 • 同期処理と同じ書き方で非同期処理できる • 中断・再開コードを生成してる • 最初のawaitまでは実は同期処理 • 真に非同期処理を必要としている場合だけContinueWith • パターン化 • awaitableパターン: Task以外をawaitできる • 非同期メソッド ビルダー パターン: Task以外を戻り値にできる

Editor's Notes

  1. ゲームだと、あんまり重たい処理だったら、事前に近似値テーブル化したりするんで、案外ゲームでCPUフルにぶん回す案件意外とないかも まあ、最近はUnity上でも動く機械学習ライブラリとかもあったりするし?
  2. http://www.prowesscorp.com/computer-latency-at-a-human-scale/
  3. staticなもの(Current)への依存がキモイとか、自動的にやるのが怖いとかいう問題はあるものの。 なので、それが嫌なら自前のtask-like型作成をしたり → UniTask
Advertisement