Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

An other world awaits you

79,310 views

Published on

C# 5.0/VB 11で導入された async/await と、
その背後にある実行インフラ
await 演算子の展開結果
await 演算子では解決できなその他の非同期
などについての話。

  • 素晴らしい資料だなと思いました。ありがとうございます。ただ41ページあたりから急激に分からなくなってしまい、「適切な同期コンテキストを拾って実行する」というのが具体的にどういう処理なのか分からず、SynchronizationContext が内部で一体なにをしているのか、自作しようと思ったらどうなるのかがよく分かりませんでした。
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here

An other world awaits you

  1. 1. An other worldawaits you避けては通れない非同期処理
  2. 2. A n o t h e r w o r l d a w a i t s y o u別世界があなたを待っています C# 5.0 C# 4.0 • Async C# 3.0 • Dynamic • LINQ C# 2.0 これ • Generics C# 1.0 asynchrony • Managed 非同期 ※VB 7~11の歴史でもある
  3. 3. A n o t h e r w o r l d a w a i t s y o u別世界があなたを待っています こんな世界  フリーズしない世界  バッテリーの持ちがいい世界 C# 5.0 • Async “今までも、俺ならできたよ”  訓練された人しかできない  訓練された人でも超大変 見合ったコストでは“できない”
  4. 4. アジェンダ 非同期処理の裏側 いろいろなタイプの非同期処理 Windows 8/WinRT/.NET 4.5時代の非同期処理
  5. 5. async/await前ふりまず初めにC# 5.0/VB 11の強力さを
  6. 6. C# 5.0 非同期処理 最大の売り  async/await 本日の主役 Caller Info  CallerFilePath/CallerLineNumber/CallerMemberName属性 細かい仕様変更/バグ修正  foreach変数  オーバーロード解決  名前付き引数の評価順序 http://ufcpp.net/study/csharp/ap_ver5.html http://msdn.microsoft.com/en-us/library/hh678682.aspx
  7. 7. C# 5.0のasync/await Taskクラス async修飾子 await演算子async Task<string> GetAsync(string url){ var client = new HttpClient(); var res = await client.GetAsync(url); var content = await res.Content.ReadAsStringAsync(); return content;} 同期処理の場合と ほぼ同じフローで非同期処理
  8. 8. await t.Wait(); • wait for t: 類義語はstay(とどまる) • スレッドを止めて待つ await t; • await t: 類義語はextpect(期待する) • スレッドを止めずにコールバックを待つ 重要: ちゃんと非同期
  9. 9. もう少し複雑な例  複数の確認ダイアログ表示 確認フローゲームで 確認 1 Yes レア アイテムですよ?アイテムを チェック 結果表示合成します No 確認 2 Yes 合成強化済みですよ? チェック No Yes 確認 3 もう強化限界ですよ? チェック No
  10. 10. 同期if (this.Check1.IsChecked ?? false){ var result = Dialog.ShowDialog("確認 1", "1つ目の確認作業"); if (!result) return false;}if (this.Check2.IsChecked ?? false){ var result = Dialog.ShowDialog("確認 2", "2つ目の確認作業"); if (!result) return false;}if (this.Check3.IsChecked ?? false){ var result = Dialog.ShowDialog("確認 3", "3つ目の確認作業"); if (!result) return false;}return true;
  11. 11. 非同期(旧)if (this.Check1.IsChecked ?? false)  画面に収まるように{ Dialog.BeginShowDialog("確認 1", "1つ目の確認作業", result => { if (!result) { onComplete(false); フォント サイズ調整 return; } if (this.Check2.IsChecked ?? false) { Dialog.BeginShowDialog("確認 2", "2つ目の確認作業", result2 => {  4ptです if (!result2) { onComplete(false); return; } if (this.Check3.IsChecked ?? false) {  ほんの84行ほど Dialog.BeginShowDialog("確認 3", "3つ目の確認作業", result3 => { onComplete(result3); }); } else onComplete(true); });  ちなみに } else if (this.Check3.IsChecked ?? false) { Dialog.BeginShowDialog("確認 3", "3つ目の確認作業", result3 => { onComplete(result3); }); }  部分部分を関数化して多少は else onComplete(true); });}else if (this.Check2.IsChecked ?? false) 整理できます{ Dialog.BeginShowDialog("確認 2", "2つ目の確認作業", result => { if (!result) { onComplete(false); return;  ダイアログ3つだからまだこの } if (this.Check3.IsChecked ?? false) { Dialog.BeginShowDialog("確認 3", "3つ目の確認作業", result3 => 程度で済んでます { onComplete(result); }); } else onComplete(true); });}else if (this.Check3.IsChecked ?? false){ Dialog.BeginShowDialog("確認 3", "3つ目の確認作業", result3 => { onComplete(result3); });}else onComplete(true);
  12. 12. 非同期(C# 5.0)if (this.Check1.IsChecked ?? false){ var result = await Dialog.ShowDialogAsync("確認 1", "1つ目の確認作業"); if (!result) return false;}if (this.Check2.IsChecked ?? false){ var result = await Dialog.ShowDialogAsync("確認 2", "2つ目の確認作業"); if (!result) return false;}if (this.Check3.IsChecked ?? false){ var result = await Dialog.ShowDialogAsync("確認 3", "3つ目の確認作業"); if (!result) return false;} • await演算子が増えただけreturn true; • ダイアログが増えても平気
  13. 13. さて、今日の議題こんなに便利なasync/awaitですが… 中身はどうなってるの?  非同期処理の実行インフラ  awaitの実現方法 awaitで万事解決?  そうでもない  並列処理  イベント型の非同期  データフロー
  14. 14. 非同期処理の実行インフラスレッド プールとかI/Oとか同期コンテキストとか
  15. 15. 非同期インフラ スレッド スレッド プール I/O完了ポート UIスレッドと同期コンテキスト
  16. 16. 非同期インフラ スレッド スレッド プール I/O完了ポート UIスレッドと同期コンテキスト
  17. 17. スレッド 非同期処理の最も低レイヤーな部分 Worker1 Worker2static void Main(){ var t = new Thread(Worker2); CPUをシェア t.Start(); 定期的に切り替え Worker1();}  スレッドはプリエンプティブ※なマルチタスク  ハードウェア タイマーを使って強制割り込み 高負荷  OSが特権的にスレッド切り替えを行う ※preemptive: 先売権のある
  18. 18. スレッド 非同期処理の最も低レイヤーな部分  ただし、高負荷for (int i = 0; i < 1000; i++){ var t = new Thread(Worker); t.Start();} 1000個の処理を同時実行  細々とした大量の処理をこなすには向かない  切り替え(コンテキスト スイッチ)のコストが高すぎる
  19. 19. スレッドを立てるコスト スレッドに紐づいたデータ  カーネル ステート: 1kBくらい  ローカル スタック: 1MBくらい イベント発生  Thread Attached/Detachedイベント
  20. 20. スレッド切り替えコスト カーネル モードに移行 レジスターの値を保存 lockを獲得 次に実行するスレッドの決定 どれも結構時間が lockを解放 かかる処理 スレッドの状態を入れ替え レジスターの値の復元 カーネル モードから復帰 ※ http://blogs.msdn.com/b/larryosterman/archive/2005/01/05/347314.aspx
  21. 21. スレッド切り替えコスト削減 切り替えの原因  一定時間経過  待機ハンドル待ち(lock獲得とか)  同期I/O待ち 対策  そもそもスレッドを立てない 自前実装大変  lock-freeアルゴリズム利用 スレッドは直接使わない  I/Oは非同期待ち スレッド プール I/O完了ポート
  22. 22. スレッド切り替えコスト削減 切り替えの原因  一定時間経過 .NETでいうと  待機ハンドル待ち(lock獲得とか) • Threadクラスは使わない  同期I/O待ち .NET for Windowsストア アプリではついに削除された • Taskクラスを使う 対策 3.5以前の場合はThraedPoolクラスやTimerクラス、 .NET IAsyncResultインターフェイス  そもそもスレッドを立てない 自前実装大変  lock-freeアルゴリズム利用 スレッドは直接使わない  I/Oは非同期待ち スレッド プール I/O完了ポート
  23. 23. 非同期インフラ スレッド スレッド プール I/O完了ポート UIスレッドと同期コンテキスト
  24. 24. 2種類のマルチタスク プリエンプティブ • ハードウェア タイマーを使って強制割り込み • OSが特権的にスレッド切り替えを行う • 利点: 公平(どんなタスクも等しくOSに制御奪われる) • 欠点: 高負荷(切り替えコストと使用リソース量) 協調的※ • 各タスクが責任を持って終了する • 1つのタスクが終わるまで次のタスクは始まらない • 利点: 低負荷 • 欠点: 不公平(1タスクの裏切りが、全体をフリーズさせる) ※cooperative
  25. 25. スレッド プール スレッドを可能な限り使いまわす仕組み  プリエンプティブなスレッド数本の上に  協調的なタスク キューを用意 スレッド プール キュー 数本のスレッド新規タスク だけ用意 タスク1 タスク2 タスクは一度キューに溜める … 空いているスレッドを探して実行 (長時間空かない時だけ新規スレッド作成)
  26. 26. スレッド プールの性能的な工夫 Work Stealing Queue  lock-free実装なローカル キュー  できる限りスレッド切り替えが起きないように グローバル ローカル ローカル キュー キュー1 キュー2 スレッド1 スレッド2 ① スレッドごとに ② キューを持つ ローカル キュー が空のとき、 他のスレッドから タスクを奪取
  27. 27. 参考: lock-freeアルゴリズムlockベース OS機能に頼った競合回避lock (_sync) (カーネル モード移行あり){ _value = SomeOperation(_value);}lock-free(interlockedベース) 競合してたらやり直すlong oldValue1, oldValue2; CPUのInterlocked命令※を利用do{ 競合頻度が低い時に高効率 oldValue1 = _value; var newValue = SomeOperation(_value); oldValue2 = Interlocked.CompareExchange( ref _value, newValue, oldValue1);}while (oldValue1 != oldValue2); ※ アトミック性を保証したCPU命令。カーネル モード移行と比べるとだいぶ低不可 interlocked: 連結した、連動した
  28. 28. 非同期インフラ スレッド スレッド プール I/O完了ポート UIスレッドと同期コンテキスト
  29. 29. おさらい 待機しちゃダメ  × lock 何もしていないのにスレッド立てっぱなし  × 同期I/O待ち • スタック(1MB)取りっぱなし • スレッド切り替えの誘発  × Thread.Sleep • スレッド プール上でも新しいスレッド が立ってしまう  ○ 非同期I/O I/O完了ポート  ○ タイマー(Task.Delay)
  30. 30. I/O Input/Output  CPUと、CPUの外との入出力 待機しちゃダメ  CPU内での計算と比べると、数ケタ遅い ユーザー入力 通信 CPU ストレージ ハードウェア タイマー
  31. 31. I/O完了ポート※ I/Oを待たない  コールバックを登録  I/O完了後、スレッド プールで続きの処理を行う アプリ あるスレッド スレッド プール タスク1コールバック登録後、 コールバック タスク2 登録すぐにスレッド上での … 処理を終了 I/O完了ポート I/O開始 I/O完了 (OSカーネル内) ハードウェア ※ I/O completion port
  32. 32. 非同期APIを使いましょう 同じ非同期処理でも Task.Run(() => req.GetResponse()); • スレッド内で同期I/O • スレッド立てっぱなし req.GetResponseAsync(); • I/O完了ポートを使って非同期I/O • コールバック登録後、スレッドを解放
  33. 33. Sleepもダメ(例: Sleep Sort) 値に比例してSleepすればソートできるんじゃね?  というネタ。×スレッド立ててSleepnew Thread(_ => { Thread.Sleep(t); q.Enqueue(x); }).Start(); 要素数分のスレッドが立つ 要素数×1MBのスタック確保 既定の設定だと、スレッド1,000個くらいで Out of Memory○タイマー利用+コールバックTask.Delay(t).ContinueWith(_ => q.Enqueue(x));
  34. 34. ダメなものは最初から提供しない Windows 8世代のAPI 50ミリ秒以上かかる可能性のあるAPIは 非同期APIのみ提供  WinRT  ファイル操作、ネットワーク、グラフィック  ランチャー、ダイアログ表示  .NET 4.5で追加されたクラス  HttpClientなど  .NET for Windows ストア アプリ  同期I/O APIやThreadクラス削除
  35. 35. 非同期インフラ スレッド スレッド プール I/O完了ポート UIスレッドと同期コンテキスト
  36. 36. シングル スレッド必須 スレッド安全なコードは高コスト いっそ、単一スレッド動作を前提に  典型例はGUI  C#/.NETに限らずたいていのGUIフレームワークはシングル スレッド動作  低レイヤーAPI(DirectXとかOpenGLとか)も、1つのス レッドからしか扱えない
  37. 37. 典型例: UIスレッド GUIは単一スレッド動作(UIスレッド)  ユーザーからの入力受け付け  画面の更新 UIスレッド 他のスレッド ユーザー グラフィック からの入力 更新 OK 処理中は 他のスレッドから 応答不可 は更新不可
  38. 38. 矛盾 シングル スレッド推奨 単一スレッドからしか UI更新できない OK そのスレッドを止める とUIフリーズ マルチ スレッド推奨
  39. 39. 解決策1. スレッド プールで重たい処理2. UIスレッドに処理を戻してからUI更新 UIスレッド 他のスレッド Task.Run 重たい処理 Dispatcher.Invoke 更新 OK 戻す役割を担うのが ディスパッチャー※ ※ dispatcher: 配送者
  40. 40. ディスパッチャーの利用例 WPFの場合※ Task.Run(() => { var result = HeavyWork(); this.Dispatcher.Invoke(() => { this.List.ItemsSource = result; }); }); • あまり意識したくないんだけども • 自動的にはやってくれないの? ※ WPF、Silverlight、WinRT XAMLでそれぞれ書き方が少しずつ違う
  41. 41. 自動化するにあたって 処理の実行場所には文脈がある  同期コンテキスト※と呼ぶ 文脈 要件 スレッド プール どのスレッドで実行しててもいい 実行効率最優先 GUI UIの更新は、UIスレッド上での実行 が必要(ディスパッチャーを経由) Web API どのスレッドで実行してもいい ただし、どのWebリクエストに対す る処理か、紐づけが必要  適切な同期コンテキストを拾って実行する必要がある ※ synchronization context もちろん自作も可能
  42. 42. 同期コンテキストの利用例 var context = SynchronizationContext.Current; Task.Run(() => { var result = HeavyWork(); context.Post(r => { this.List.ItemsSource = (IEnumerable<int>)r; }, result); }); 文脈に応じた適切な実行方法をしてくれる • WPFの場合はディスパッチャーへのPost あとは、ライブラリの中で自動的に同期コンテキ ストを拾ってくれれば万事解決…?
  43. 43. 完全自動化無理でした スレッド プール中で同期コンテキストを拾うと、 スレッド プールにしか返ってこない  BackgroundWorkerのDoWork内で、別の BackgroundWorkerを作ると破綻 必ずUIスレッドに処理を戻すと実行効率悪い  ぎりぎりまでスレッド プール上で実行したい ネイティブ⇔.NETをまたげない  WinRT XAML UIだと自動化無理 つまるところ、JavaScriptとかでそれができているのは • 単一UIフレームワーク • 実行効率度外視
  44. 44. なので、ある程度の自由を TaskSchdulerの選択 既定動作(スレッド プール) Task.Run(() => HeavyWork()) .ContinueWith(t => { // スレッド プール上で実行される }); 同期コンテキストを拾う Task.Run(() => HeavyWork()) .ContinueWith(t => { // UI スレッド上で実行される }, TaskScheduler.FromCurrentSynchronizationContext());
  45. 45. ちなみに、awaitは 既定で同期コンテキストを拾う 既定動作(同期コンテキストを拾う) var result = await Task.Run(() => HeavyWork()); 挙動の変更(同期コンテキストを拾わない) var result = await Task.Run(() => HeavyWork()) .ConfigureAwait(false); awaitした場所で同期コンテキストを拾う  (ライブラリ内でなく)利用側で  なので、ネイティブAPIを使っていても、ちゃんと拾える
  46. 46. いろいろな非同期awaitも万能じゃない
  47. 47. awaitが解決するもの(1) 1往復の、pull型非同期処理 例: 前述のGUIでの非同期処理 UIスレッド 他のスレッド Task.Run 重たい処理 await var result = await Task.Run(() => HeavyWork());
  48. 48. awaitが解決するもの(2) 1往復の、pull型非同期処理 非同期I/Oも好例 UIスレッド I/O完了ポート Task.Run I/O開始 await I/O完了 var result = await client.GetAsync();
  49. 49. awaitが解決するもの(3) 1往復の、pull型非同期処理 ダイアログ ウィンドウの表示なんかも UIスレッド GUIフレームワーク ダイアログ表示 ユーザーがダイアログを操作 OK await var result = await dialog.ShowAsync(); 「UIスレッドの入れ子」みたいなことができないので、 一度処理を抜けなきゃいけない
  50. 50. awaitが解決しないもの 並列処理 イベント型非同期 そもそも制御フローを書きにくいもの
  51. 51. 並列処理 マルチコアCPUを使いきりたい  Parallelクラス Parallel.ForEach(data, x => { // 並列に実行したい処理 });  Parallel LINQ var results = data.AsParallel() .Select(x => /* 並列に実行したい処理 */);
  52. 52. イベント型非同期処理 複数件、push型 処理側 発生側  センサーAPI ハンドラー登録  サーバーからのpush通知 イベント発生  ユーザー操作への応答も ある意味では非同期処理 イベント発生 イベント発生void Init(){ var sensor = Accelerometer.GetDefault(); イベント発生 sensor.Shaken += sensor_Shaken;}void sensor_Shaken( Accelerometer sender, AccelerometerShakenEventArgs args){ // イベント処理}
  53. 53. WinRTのイベント型非同期処理 ネイティブの向こう側で起こっていること  同期コンテキストを自動的に拾えない void Init() { var sensor = Accelerometer.GetDefault(); sensor.Shaken += sensor_Shaken; } void sensor_Shaken( Accelerometer sender, AccelerometerShakenEventArgs args) { // イベント処理 UIを更新するなら明示的な } ディスパッチャー利用必須
  54. 54. WinRTのイベント型非同期処理 Rx※とか使うのがいいかも var sensor = Accelerometer.GetDefault(); Observable.FromEventPattern(sensor, "Shaken") .ObserveOn(SynchronizationContext.Current) .Subscribe(args => { // イベント処理 TaskSchedulerと同じ感覚 }); ※ Reactive Extensions http://msdn.microsoft.com/en-us/data/gg577609 NuGet取得可・Windowsストア アプリでの利用可
  55. 55. 制御フローを書きにくいもの awaitは、同期と同じように非同期を書ける そもそも同期でも書きにくいものは苦手  2次元的なデータフローとか  ステートマシンとか
  56. 56. 制御フローを書きにくいもの WFとか  http://msdn.microsoft.com/en-us/vstudio/aa663328 TPL Dataflowとか  http://msdn.microsoft.com/en-us/devlabs/gg585582.aspx
  57. 57. how to awaitasync/awaitの中身
  58. 58. 非同期処理 おさらい 待機しちゃダメ task.ContinueWith( コールバック);  コールバック  中断と再開 やるべきことは イテレーターと同じ 同期コンテキスト
  59. 59. イテレーター 中断と再開 class MethodEnumerator : IEnumerator<int> { public int Current { get; private set; } private int _state = 0; public bool MoveNext() { switch (_state) { case 0: IEnumerable<int> Method() { Current = 1; _state = 1; yield return 1; return true; case 1: Current = 2; yield return 2; _state = 2; return true; case 2: default: } return false; } } }
  60. 60. イテレーター 中断と再開 class MethodEnumerator : IEnumerator<int> { public int Current { get; private set; } private int _state = 0; public bool MoveNext() { switch (_state) { case 0: 状態の記録 IEnumerable<int> Method() Current = 1; { Current = 1; yield return 1; _state = 1; _state = 1; return true; return 1: case true; 中断 case 1: = 2; Current _state = 2; return true; 再開用のラベル yield return 2; case 2: default: } return false; } } }
  61. 61. awaitの展開結果(コンセプト) コンセプト的には イテレーター + ContinueWith 状態の記録 _state = 1; if (!task1.IsCompleted) async Task<int> Method() { { task1.ContinueWith(a); var x = await task1; var y = await task2; return; 中断 } } case 1: 再開用のラベル var x = task1.Result; 結果の受け取り
  62. 62. awaitの展開結果 実際はもう少し複雑  Awaiterというものを介していたり(Awaitableパターン) _state = 1; var awaiter1 = task1.GetAwaiter(); if (!awaiter1.IsCompleted) { awaiter1.OnCompleted(a); • こいつが同期コンテキスト return; を拾い上げていたりする } • Awaiterを自作することで、 case 1: awaitの挙動を変更可能 var x = awaiter1.GetResult(); • Task以外もawait可能
  63. 63. 逆に C# 5.0が使えなくても  イテレーターを使ってawaitに似た非同期処理可能 そこそこめんどくさい C#が使えなくても  IEnumerableを自作する程度の手間でawaitに似た(略 絶望的にめんどくさい けど、性能は出ると思う C#使わせてください。ほんとお願いします。
  64. 64. まとめ
  65. 65. まとめ スレッド(Thread)は直接使わない  スレッド プール(Task)を使う await  wait(stay)と違って、await(expect)  イテレーター(中断と再開) + ContinueWith(コールバック)  同期コンテキスト awaitにだって…できないことは…ある  並列処理、イベント型非同期  そもそも同期で書きにくいもの

×