Your SlideShare is downloading. ×
0
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
非同期処理の基礎
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×
Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Text the download link to your phone
Standard text messaging rates apply

非同期処理の基礎

41,990

Published on

2014/5/10 VSハッカソン 非同期勉強会 にて発表

2014/5/10 VSハッカソン 非同期勉強会 にて発表

Published in: Technology
1 Comment
122 Likes
Statistics
Notes
No Downloads
Views
Total Views
41,990
On Slideshare
0
From Embeds
0
Number of Embeds
18
Actions
Shares
0
Downloads
194
Comments
1
Likes
122
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. 非同期処理の 基礎知識 岩永 信之
  • 2. 今日話すこと • 非同期処理がらみの良い書き方/悪い書き方 • それがなぜ良い/悪い • 突き詰めるとCPUやOSレベルの話に
  • 3. 非同期処理の書き方 良い例・悪い例を紹介 先に事例紹介(良い・悪い理由は後ほど)
  • 4. ThreadよりもTask for (int i = 0; i < num; i++) { var t = new Thread(_ => b[i] = F(a[i]) ); } for (int i = 0; i < num; i++) { Task.Run(() => b[i] = F(a[i]) ); } ×悪い例 ○良い(まだマシ※な)例 データの数だけ スレッド作成 Threadでなく Task利用 ※ この場合、ParallelクラスやParallel.Enumerableクラスが使いやすい
  • 5. ThreadよりもTask for (int i = 0; i < num; i++) { var t = new Thread(_ => b[i] = F(a[i]) ); } for (int i = 0; i < num; i++) { Task.Run(() => b[i] = F(a[i]) ); } ×悪い例 ○良い(まだマシ※な)例 データの数だけ スレッド作成 Threadでなく Task利用 ※ この場合、ParallelクラスやParallel.Enumerableクラスが使いやすい 題材 • スレッドのコスト • スレッド プール
  • 6. 非同期I/O using (var r = new StreamReader("some.txt")) { var t = r.ReadToEndAsync(); Console.WriteLine(await t); } using (var r = new StreamReader("some.txt")) { var t = Task.Run(() => r.ReadToEnd()); Console.WriteLine(await t); } Task.Run + 同期I/O 非同期I/O用メソッド ×悪い例 ○良い例
  • 7. 非同期I/O using (var r = new StreamReader("some.txt")) { var t = r.ReadToEndAsync(); Console.WriteLine(await t); } using (var r = new StreamReader("some.txt")) { var t = Task.Run(() => r.ReadToEnd()); Console.WriteLine(await t); } Task.Run + 同期I/O 非同期I/O用メソッド ×悪い例 ○良い例 題材 • CPU-boundとI/O-bound • I/O完了ポート
  • 8. データ競合 var count = 0; Parallel.For(0, num, i => { ++count; }); var count = 0; Parallel.For(0, num, i => { lock (sync) ++count; }); 期待通りに動かない count == numにならない lockをかけるととりあえず 期待通りにはなる ×悪い例 ○良い(まだマシ※な)例 ※ Interlocked.Incrementメソッド使えばlockなしでスレッド安全にインクリメントできる 性能を考えると、次節のスレッド ローカルを使う方がいい
  • 9. データ競合 var count = 0; Parallel.For(0, num, i => { ++count; }); var count = 0; Parallel.For(0, num, i => { lock (sync) ++count; }); 期待通りに動かない count == numにならない lockをかけるととりあえず 期待通りにはなる ×悪い例 ○良い(まだマシ※な)例 ※ Interlocked.Incrementメソッド使えばlockなしでスレッド安全にインクリメントできる 性能を考えると、次節のスレッド ローカルを使う方がいい 題材 • 競合が起きる理由 • lock
  • 10. スレッド ローカル var count = 0; Parallel.For(0, num, () => 0, (i, state, localCount) => localCount + 1, localCount => count += localCount ); var count = 0; Parallel.For(0, num, i => { lock (sync) ++count; }); ×悪い例 ○良い例 (さっきの「マシな例」) lockしてスレッド間で同じ データを読み書き スレッドごとに別計算 最後に集計
  • 11. スレッド ローカル var count = 0; Parallel.For(0, num, () => 0, (i, state, localCount) => localCount + 1, localCount => count += localCount ); var count = 0; Parallel.For(0, num, i => { lock (sync) ++count; }); ×悪い例 ○良い例 (さっきの「マシな例」) lockしてスレッド間で同じ データを読み書き スレッドごとに別計算 最後に集計 題材 • スレッド並列 • 並列化しやすいアルゴリズム
  • 12. イベントの実装(前置き) event EventHandler Disposed; 自動実装イベント event EventHandler Disposed { add { _disposed += value; } remove { _disposed -= value; } } private EventHandler _disposed; こんな意味合いのコードに対し て、 「スレッド安全」が求められる (C#の規格上そう定めてある) C#のイベント 「自動実装」の結果 (意味的には)
  • 13. イベントの実装 [MethodImpl(MethodImplOptions.Synchronized)] add { _disposed += value; } lock (this)相当 add { EventHandler handler2; var disposed = _disposed; do { handler2 = disposed; var handler3 = (EventHandler)Delegate.Combine(handler2, va disposed = Interlocked.CompareExchange(ref _disposed, hand } while (disposed != handler2); } ×C# 3.0までの実装(悪い例) ○C# 4.0以降の実装(良い例) lock-free※アルゴリズム ※ lockを使わずに競合を避けること
  • 14. イベントの実装 [MethodImpl(MethodImplOptions.Synchronized)] add { _disposed += value; } lock (this)相当 add { EventHandler handler2; var disposed = _disposed; do { handler2 = disposed; var handler3 = (EventHandler)Delegate.Combine(handler2, va disposed = Interlocked.CompareExchange(ref _disposed, hand } while (disposed != handler2); } ×C# 3.0までの実装(悪い例) ○C# 4.0以降の実装(良い例) 題材 • interlocked命令 • lock-freeアルゴリズム lock-free※アルゴリズム ※ lockを使わずに競合を避けること
  • 15. 基礎 今の事例はいったん置いておいて CPUとかOSレベルの話を
  • 16. CPU まず、CPUの動作について
  • 17. CPU = 演算回路+記憶領域 ALU メイン・メモリ データを格納 高速・小容量 加減乗除などの 演算を実行† データを格納 低速・大容量 † arithmetic logic unit 演算回路記憶領域
  • 18. CPU = 演算回路+記憶領域 ALU メイン・メモリ 記憶領域には階層がある 高速小容量 ⇔ 低速大容量 演算回路と直接つながってるのは、 1番高速で、1番小容量の記憶
  • 19. CPUの動作例 • 高級言語的に1ステートメントでも… ALU メイン・メモリ ++count; ① mov eax, [04D74010h] ② inc eax ③ mov [04D74010h], eax C# CPU命令 コンパイル
  • 20. CPUの動作例 ① 読み込み ALU メイン・メモリ ① mov eax, [04D74010h] ② inc eax ③ mov [04D74010h], eax ++count; C# CPU命令 コンパイル
  • 21. CPUの動作例 ② 演算 ALU メイン・メモリ ① mov eax, [04D74010h] ② inc eax ③ mov [04D74010h], eax +1 ++count; C# CPU命令 コンパイル
  • 22. CPUの動作例 ③ 書き出し ALU メイン・メモリ ① mov eax, [04D74010h] ② inc eax ③ mov [04D74010h], eax ++count; C# CPU命令 コンパイル
  • 23. CPUの動作例 ③ 書き出し ALU メイン・メモリ ① mov eax, [04D74010h] ② inc eax ③ mov [04D74010h], eax ++count; C# CPU命令 コンパイル ポイント • 単純な処理でも複数命令からなる • 読み込み、演算、書き出し • 上から順に逐次実行
  • 24. 注意: 読み込みの原子性※ ALU メイン・メモリ 64bit データ ALU メイン・メモリ データ64bit ※ atomicity: 不可分性。中途半端な不正な状態を起こさないこと 64bit CPUの場合 32bit CPUの場合 1命令完結 2命令必要 命令と命令の間に割り込まれると データが半分しか読まれてない状態になる
  • 25. 割り込み CPUと、CPUの外の世界(ハードウェア割り込み) 特権モード(ソフトウェア割り込み)
  • 26. CPUの外の世界 • 当然、CPUだけでは何もできない ALU メイン・メモリ CPU 周辺機器 割り込み信号
  • 27. 割り込み • 外部ハードウェアから「割り込み信号」が来る • 信号を受け取ると、実行中の処理を止めて、いった ん別処理をする 命令列 別の処理 外部ハードウェアから の信号を受け取って、 処理を中断 再開 mov eax, [04D74010h] inc eax mov [04D74010h], eax ……
  • 28. 割り込みタイミング • 割り込みはどこでかかるかわからない mov eax, [04D74010h] inc eax mov [04D74010h], eax …… ここで割り込まれる かもしれないし ここも ここも ここもありえる
  • 29. ハードウェア タイマー • 一定間隔で割り込み信号を送ってくるハード ウェアがある • スレッドで使う(詳細は後述) 命令列 ハードウェアタイマー 割り込み 割り込み 割り込み … 別の処理 別の処理 別の処理
  • 30. ソフトウェア割り込み • 割り込み命令 命令列 別の処理 自分自身で割り込みを 起こせる命令がある 再開 割り込み発生命令 ……
  • 31. モード切り替え • 何に使うかというと、モード切り替え 命令列 別の処理割り込み発生命令 …… 通常のセキュリティ レベルで動作 特権的なセキュリティ レベルで動作 異なるモードで動作
  • 32. 特権モード ユーザー モード • 一般のプログラ ムに認められる セキュリティ レ ベル • アクセスできる メモリに制限が ある 特権モード※ • OSが使うセキュ リティ レベル • 制限がかからな い モード移行にはそれなりのコストが発生 ※ OSのカーネルが使うんで、カーネル モード(kernel mode)ともいう
  • 33. CPUの高度化 キャッシュ メモリ マルチコアCPU
  • 34. • 記憶領域の階層は多段 メイン メモリ 2次キャッシュ メモリ キャッシュ ALU キャッシュ メモリ 高速 小容量 低速 大容量 速度差が大きすぎる キャッシュ1段ごとに1桁くらい遅い
  • 35. • 記憶領域の階層は多段 メイン メモリ 2次キャッシュ メモリ キャッシュ ALU キャッシュ メモリ 高速 小容量 低速 大容量 このサイズに収まる範囲で 読み書きする分には高速 広範囲にデータを読み書き すると、低速なメモリへの 読み書きが発生
  • 36. • コアごとにキャッシュ持ってたり メイン メモリ 2次キャッシュ メモリ マルチコア ALU キャッシュ メモリ ALU キャッシュ メモリ ALU キャッシュ メモリ
  • 37. • コアで同じデータを読み書きすると メイン メモリ 2次キャッシュ メモリ マルチコア ALU キャッシュ メモリ ALU キャッシュ メモリ ALU キャッシュ メモリ 同じブロックを読み書 きしているつもりでも 実際には別の場所にある 1段下(低速)に書き戻されて ないと正しい値が取れない = 1桁遅い
  • 38. • 非均一な読み書き速度 NUMA※ ALU キャッシュ メモリ ALU キャッシュ メモリ ALU キャッシュ メモリ ※ non-uniform memory access: 非均一なメモリアクセス メモリ ノード1 メモリ ノード2 メモリ ノード3 高速 アクセスはできる ものの、低速
  • 39. • 非均一な読み書き速度 NUMA※ ALU キャッシュ メモリ ALU キャッシュ メモリ ALU キャッシュ メモリ ※ non-uniform memory access: 非均一なメモリアクセス メモリ ノード1 メモリ ノード2 メモリ ノード3 高速 アクセスはできる ものの、低速 ポイント • コアをまたいだデータ読み書きは かなり低速
  • 40. スレッド マルチタスク CPUのシェア
  • 41. マルチタスク • コンピューター内で複数のタスクが同時に動作 • CPUコア数に制限されない タスク1 タスク2 タスク3 … タスクの動作期間 実際にCPUを使って 動いている期間 1つのCPUコアを複数の タスクがシェアしてる 問題は • どうやって他のタスクにCPUを譲るか • 誰がどうスケジューリングするか
  • 42. 2種類のマルチタスク † preemptive: 専買権を持つ、横取りする ※cooperative • ハードウェア タイマーを使って強制割り込み • OSが特権的にスレッド切り替えを行う • ○利点: 公平 (どんなタスクも等しくOSに制御奪われる) • ×欠点: 高負荷 (切り替えコストと使用リソース量が多い) プリエンプティブ† • 各タスクが責任を持って終了する • 1つのタスクが終わるまで次のタスクは始まらない • ○利点: 低負荷 • ×欠点: 不公平 (1タスクの裏切りが、全体をフリーズさせる) 協調的※ なのでスレッドはこっち これが致命的 ただ、問題はこれ
  • 43. スレッドを立てるコスト※ • スレッドに紐づいたデータ • カーネル ステート: 1kBくらい • ローカル スタック: 1MBくらい • イベント発生 • Thread Attached/Detachedイベント ※ Windowsの場合
  • 44. スレッド切り替えコスト • 直接的なコスト • 特権モードへの移行・復帰 • レジスターの保存・復元 • 次に実行するスレッドの決定 • スレッドの状態を入れ替え • 間接的なコスト • キャッシュ ミス どれも性能への インパクト大きい
  • 45. スレッドは高コスト • 細々としたタスクを大量にこなすには向かない for (int i = 0; i < 1000; i++) { var t = new Thread(Worker); t.Start(); } 大量の処理をスレッド実行 リソース消費大 切り替え頻発 …
  • 46. スレッド プール • スレッドを可能な限り使いまわす仕組み • プリエンプティブなスレッド数本の上に • 協調的なタスク キューを用意 スレッド プール キュー タスク1 タスク2 … 数本※のスレッ ドだけ用意 空いているスレッドを探して実行 (長時間空かない時だけ新規スレッド作成) 新規タスク タスクは一度 キューに溜める ※ 理想的にはCPUのコア数分だけ
  • 47. スレッド プールの性能向上 • Work Stealing Queue • lock-free実装(後述)なローカル キュー • できる限りスレッド切り替えが起きない作り ローカル キュー1 ローカル キュー2 スレッド1 スレッド2 グローバル キュー ① スレッドごとに キューを持つ まず自分用の キューからタスク実行 ② ローカル キュー が空のとき、 他のスレッドから タスクを奪取
  • 48. スレッド プールの性能向上 • Work Stealing Queue • lock-free実装(後述)なローカル キュー • できる限りスレッド切り替えが起きない作り ローカル キュー1 ローカル キュー2 スレッド1 スレッド2 グローバル キュー ① スレッドごとに キューを持つ まず自分用の キューからタスク実行 ② ローカル キュー が空のとき、 他のスレッドから タスクを奪取 ポイント • スレッドは高コスト • Threadくらすはこっち • スレッド プールの利用推奨 • Taskクラスはこっち
  • 49. I/O完了ポート 外部ハードウェアからの応答を待つ
  • 50. CPUの外の世界は遅い • 実行速度が全然違う ALU メイン・メモリ CPU 周辺機器 数千~ 下手すると数万、数億倍遅い
  • 51. 2種類の負荷 • CPU-bound (CPUが性能を縛る) • マルチコアCPUの性能を最大限引き出したい • UIスレッドを止めたくない • I/O-bound (I/O※が性能を縛る) • ハードウェア割り込み待つだけ • CPUは使わない • スレッドも必要ない ※ Input/Output: 外部ハードウェアとのやり取り(入出力)
  • 52. I/O完了待ち • I/O-boundな処理にスレッドは不要 あるスレッド 要求 応答 この間何もしないのに スレッドを確保し続け るのはもったいない
  • 53. I/O完了ポート※ • スレッドを確保せずI/Oを待つ仕組み • コールバックを登録して、割り込みを待つ • コールバック処理はスレッド プールで スレッド プール タスク1 タスク2 … ※ I/O completion port あるスレッドアプリ I/O完了ポート ハードウェア I/O開始 I/O完了 コールバック 登録 コールバック登録後、 すぐにスレッド上での 処理を終了 割り込み信号
  • 54. I/O完了ポート※ • スレッドを確保せずI/Oを待つ仕組み • コールバックを登録して、割り込みを待つ • コールバック処理はスレッド プールで スレッド プール タスク1 タスク2 … ※ I/O completion port あるスレッドアプリ I/O完了ポート ハードウェア I/O開始 I/O完了 コールバック 登録 コールバック登録後、 すぐにスレッド上での 処理を終了 割り込み信号 ポイント • I/O-boundな処理にスレッドを使っちゃダメ • I/O用の非同期メソッドが用意されてる (内部的にI/O完了ポートを利用)
  • 55. 事例に戻って 良い例・悪い例の理由
  • 56. ThreadよりもTask for (int i = 0; i < num; i++) { var t = new Thread(_ => b[i] = F(a[i]) ); } for (int i = 0; i < num; i++) { Task.Run(() => b[i] = F(a[i]) ); } ×悪い例 ○良い(まだマシ※な)例 データの数だけ スレッド作成 Threadでなく Task利用 ※ この場合、ParallelクラスやParallel.Enumerableクラスが使いやすい
  • 57. おさらい • Threadクラス • Windowsの生スレッド • = プリエンプティブなマルチタスク • 当然重たい • 特権モード移行、レジスター退避、… • Taskクラス • スレッド プールを利用 • 必要な分だけスレッド使う 推奨 スレッドは生で使 うものじゃない
  • 58. 非同期I/O using (var r = new StreamReader("some.txt")) { var t = r.ReadToEndAsync(); Console.WriteLine(await t); } using (var r = new StreamReader("some.txt")) { var t = Task.Run(() => r.ReadToEnd()); Console.WriteLine(await t); } Task.Run + 同期I/O 非同期I/O用メソッド ×悪い例 ○良い例
  • 59. おさらい • I/O-boundな処理のためにスレッドを専有し ちゃダメ • I/O完了ポート使う • ハードウェアからの割り込みをイベント処理 • 標準ライブラリの~Async系のメソッドはこれを利 用 非推奨 Task.Run(() => r.ReadToEnd()); 推奨 r.ReadToEndAsync();
  • 60. おまけ: SleepよりもDelay Task.Delay((int)(x * Scale)) .ContinueWith((_, state) => { sorted.Enqueue((double)state); }, x); var t = new Thread(state => { var value = (double)state; Thread.Sleep((int)(value * Scale)); sorted.Enqueue(value); }); t.Start(x); スレッドを立ててから Thread.Sleepで休止 Task.Delayで休止 ×悪い例 ○良い例 • 何もしないのにスレッド を確保し続ける • タイマー利用 (ハードウェア割り込み) • スレッドを確保しない
  • 61. データ競合 var count = 0; Parallel.For(0, num, i => { ++count; }); var count = 0; Parallel.For(0, num, i => { lock (sync) ++count; }); 期待通りに動かない count == numにならない lockをかけるととりあえず 期待通りにはなる ×悪い例 ○良い(まだマシ※な)例 ※ Interlocked.Incrementメソッド使えばlockなしでスレッド安全にインクリメントできる 性能を考えると、次節のスレッド ローカルを使う方がいい
  • 62. おさらい • スレッドはハードウェア タイマーの割り込み を使ってる • 割り込みはいつかかるかタイミング不定 • 単純なコードでも、CPUレベルでは複数命令 ++count; C# CPU命令コンパイル ① 読み mov eax, [04D74010h] ② 計算 inc eax ③ 書き mov [04D74010h], eax
  • 63. 競合 • 複数のスレッドで同じ場所を読み書きすると… シングル スレッド マルチ スレッド 読み 計算 書き 読み 計算 書き 読み 計算 読み 計算 読み 計算 書き 読み 計算 書き …… …… switch switch 書き込み終わる前にスレッド が切り替わることがある • 計算前の値を再度読んじゃう だいぶ昔の計算結果で上書き • 別スレッドで計算してた分が消える
  • 64. lock • 競合回避のためにlock (鍵)をかける switch switch lock獲得 lock解放 lock獲得 獲得失敗 他のスレッドがlock獲得 しようとすると失敗する その場でいったんスレッ ド実行を停止 読み 計算 書き
  • 65. lock • 競合回避のためにlock (鍵)をかける switch lock獲得 lock解放 獲得~解放の間は、同時に2つ以上 のスレッドで実行されなくなる読み 計算 書き lock獲得 lock解放 読み 計算 書き
  • 66. lockの仕組み • OSのスレッド スケジューラーに依頼 • lockがかかっていると、スケジューラーがスレッド 実行を止める • 特権モードが必要 • 無駄にスレッド切り替えが増える 高コスト switch switch lock獲得 lock解放 lock獲得 獲得失敗 読み 計算 書き スレッド切り替えも高コスト
  • 67. スレッド ローカル var count = 0; Parallel.For(0, num, () => 0, (i, state, localCount) => localCount + 1, localCount => count += localCount ); var count = 0; Parallel.For(0, num, i => { lock (sync) ++count; }); ×悪い例 ○良い例 (さっきの「マシな例」) lockしてスレッド間で同じ データを読み書き スレッドごとに別計算 最後に集計
  • 68. おさらい • lockは高コスト • 特権モードが必要 • 無駄なスレッド切り替え発生 • スレッド間のデータ共有は高コスト • (特に書き込み) • キャッシュからメイン メモリへの書き戻し • コアごとのキャッシュへの伝搬 • NUMA (非均一メモリ アクセス)
  • 69. スレッドごとの独立性 • 性能を求めるなら • スレッド間での同じ場所の読み書きをなくす • Parallelクラスにはそのためのオーバーロードあり var count = 0; Parallel.For(0, num, () => 0, (i, state, localCount) => localCount + 1, localCount => count += localCount ); スレッドごとに別々に+1 最後にスレッドごとの結果を足す
  • 70. 気を付けるポイント • 独立に計算できないと、並列化の利益少ない • 順序依存とかがあると無理 交換法則、結合法則が大事 これが成り立たない演算は 並列化に向かない さっきの++countループの例だと この辺りに気を使って アルゴリズム考える必要あり 和を2つに分解 (分解しても計算結果が一緒)
  • 71. イベントの実装 [MethodImpl(MethodImplOptions.Synchronized)] add { _disposed += value; } lock (this)相当 add { EventHandler handler2; var disposed = _disposed; do { handler2 = disposed; var handler3 = (EventHandler)Delegate.Combine(handler2, va disposed = Interlocked.CompareExchange(ref _disposed, hand } while (disposed != handler2); } lock-free※アルゴリズム ×C# 3.0までの実装(悪い例) ○C# 4.0以降の実装(良い例) ※ OS機能(特権モード必要)のlockを使わずに競合を避けること
  • 72. おさらい • lockは高コスト(再) • 特権モードが必要 • 無駄なスレッド切り替え発生 • 旧実装はlock [MethodImpl(MethodImplOptions.Synchronized)] add { _disposed += value; } add { lock(this) { _disposed += value; } } ※ lock(this)は性能面以外にも問題あり 旧コードはいろんな意味でレガシー ※
  • 73. 新実装 • lockなしで競合回避するためのアルゴリズム add { EventHandler handler2; var disposed = _disposed; do { handler2 = disposed; var handler3 = (EventHandler)Delegate.Combine(handler2, valu disposed = Interlocked.CompareExchange(ref _disposed, handle } while (disposed != handler2); } こいつがポイント
  • 74. Interlocked • CPUには「必ず原子的※に実行する保証付き」 な命令がいくつかある(interlocked命令) • .NETの場合、Interlockedクラスを利用 ※ atomic: 不可分な。途中で他のスレッドなどに割り込まれない保証 var count = 0; Parallel.For(0, num, i => { Interlocked.Increment(ref count); }); 例: 原子性保証付きのインクリメント 挙動的にはlock付きの++countと同じ
  • 75. lockとinterlocked命令 • lockはOS機能 • 特権モード移行が必要 • スレッド切り替えの機会を増やす • 任意の処理をlockできる • Interlocked命令は単なるCPU命令 • 特権モード不要 • lockよりはだいぶ低コスト • とはいえ、普通の命令と比べると1桁くらいは遅い • 単純な処理しか用意されてない • インクリメントとか
  • 76. CAS※ • 特に重要なのがCAS命令 • .NET的にはInterlocked.CompareExchangeメソッド ※ compare and swapの略。比較しながら交換 Intel CPUの命令名称的には compare exchange int CompareExchange(ref int loc, int value, int comp) { int ret = loc; if (ret == comp) loc = value; return ret; } ↓こういう意味のコードを原子性保証付きで実行する命令 比較しながらの値の交換
  • 77. CAS • 何が重要かというと • 競合が起きたことを検知できる • 「競合を避ける」よりははるかに低コスト int CompareExchange(ref int loc, int value, int comp) { int ret = loc; if (ret == comp) loc = value; return ret; } 競合してたら元のlocとは違う値が返る
  • 78. 新実装がやってること • 競合が見つかったらやりなおし add { EventHandler handler2; var disposed = _disposed; do { handler2 = disposed; var handler3 = (EventHandler)Delegate.Combine(handler2, valu disposed = Interlocked.CompareExchange(ref _disposed, handle } while (disposed != handler2); } 競合検知しながらの交換 競合してたら最初からやりなおし いわゆる「楽観的排他制御」 eventの+=が競合することはほとんどないので、ほぼ1発
  • 79. 注意 • この手のlock-freeアルゴリズムは書くの大変 • 書いたはいいけど、テストが大変 • 普通はライブラリまかせ • Taskクラス(が使ってるスレッド プール) • System.Collections.Concurrent名前空間内のクラス • eventの自動実装(C# 4.0以降) 内部的にlock-free実装なものの例:
  • 80. まとめ • スレッドは高コスト • スレッド プール(Taskクラス)推奨 • I/O-boundな処理にスレッド不要 • ~Asyncメソッドの利用推奨 • 競合 • lockが必要 • できればlockも避ける • そもそもスレッド間でデータ共有しないアルゴリズム • lock-freeアルゴリズム(interlocked命令) • 競合を避けるよりは、検知してやり直しの方が低コスト

×