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.
非同期処理の
基礎知識
岩永 信之
今日話すこと
• 非同期処理がらみの良い書き方/悪い書き方
• それがなぜ良い/悪い
• 突き詰めるとCPUやOSレベルの話に
非同期処理の書き方
良い例・悪い例を紹介
先に事例紹介(良い・悪い理由は後ほど)
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++)
{
...
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++)
{
...
非同期I/O
using (var r = new StreamReader("some.txt"))
{
var t = r.ReadToEndAsync();
Console.WriteLine(await t);
}
using (var...
非同期I/O
using (var r = new StreamReader("some.txt"))
{
var t = r.ReadToEndAsync();
Console.WriteLine(await t);
}
using (var...
データ競合
var count = 0;
Parallel.For(0, num, i =>
{
++count;
});
var count = 0;
Parallel.For(0, num, i =>
{
lock (sync)
++cou...
データ競合
var count = 0;
Parallel.For(0, num, i =>
{
++count;
});
var count = 0;
Parallel.For(0, num, i =>
{
lock (sync)
++cou...
スレッド ローカル
var count = 0;
Parallel.For(0, num,
() => 0,
(i, state, localCount) => localCount + 1,
localCount => count += lo...
スレッド ローカル
var count = 0;
Parallel.For(0, num,
() => 0,
(i, state, localCount) => localCount + 1,
localCount => count += lo...
イベントの実装(前置き)
event EventHandler Disposed; 自動実装イベント
event EventHandler Disposed
{
add { _disposed += value; }
remove { _dis...
イベントの実装
[MethodImpl(MethodImplOptions.Synchronized)]
add { _disposed += value; }
lock (this)相当
add
{
EventHandler handler2...
イベントの実装
[MethodImpl(MethodImplOptions.Synchronized)]
add { _disposed += value; }
lock (this)相当
add
{
EventHandler handler2...
基礎
今の事例はいったん置いておいて
CPUとかOSレベルの話を
CPU
まず、CPUの動作について
CPU = 演算回路+記憶領域
ALU
メイン・メモリ
データを格納
高速・小容量
加減乗除などの
演算を実行†
データを格納
低速・大容量
† arithmetic logic unit
演算回路記憶領域
CPU = 演算回路+記憶領域
ALU
メイン・メモリ
記憶領域には階層がある
高速小容量 ⇔ 低速大容量
演算回路と直接つながってるのは、
1番高速で、1番小容量の記憶
CPUの動作例
• 高級言語的に1ステートメントでも…
ALU
メイン・メモリ
++count;
① mov eax, [04D74010h]
② inc eax
③ mov [04D74010h], eax
C#
CPU命令
コンパイル
CPUの動作例
① 読み込み
ALU
メイン・メモリ
① mov eax, [04D74010h]
② inc eax
③ mov [04D74010h], eax
++count;
C#
CPU命令
コンパイル
CPUの動作例
② 演算
ALU
メイン・メモリ
① mov eax, [04D74010h]
② inc eax
③ mov [04D74010h], eax
+1
++count;
C#
CPU命令
コンパイル
CPUの動作例
③ 書き出し
ALU
メイン・メモリ
① mov eax, [04D74010h]
② inc eax
③ mov [04D74010h], eax
++count;
C#
CPU命令
コンパイル
CPUの動作例
③ 書き出し
ALU
メイン・メモリ
① mov eax, [04D74010h]
② inc eax
③ mov [04D74010h], eax
++count;
C#
CPU命令
コンパイル
ポイント
• 単純な処理でも複...
注意: 読み込みの原子性※
ALU
メイン・メモリ
64bit データ
ALU
メイン・メモリ
データ64bit
※ atomicity: 不可分性。中途半端な不正な状態を起こさないこと
64bit CPUの場合 32bit CPUの場合
1命...
割り込み
CPUと、CPUの外の世界(ハードウェア割り込み)
特権モード(ソフトウェア割り込み)
CPUの外の世界
• 当然、CPUだけでは何もできない
ALU
メイン・メモリ
CPU 周辺機器
割り込み信号
割り込み
• 外部ハードウェアから「割り込み信号」が来る
• 信号を受け取ると、実行中の処理を止めて、いった
ん別処理をする
命令列
別の処理
外部ハードウェアから
の信号を受け取って、
処理を中断
再開
mov eax, [04D74010h...
割り込みタイミング
• 割り込みはどこでかかるかわからない
mov eax, [04D74010h]
inc eax
mov [04D74010h], eax
……
ここで割り込まれる
かもしれないし
ここも
ここも
ここもありえる
ハードウェア タイマー
• 一定間隔で割り込み信号を送ってくるハード
ウェアがある
• スレッドで使う(詳細は後述)
命令列
ハードウェアタイマー
割り込み
割り込み
割り込み
…
別の処理
別の処理
別の処理
ソフトウェア割り込み
• 割り込み命令
命令列
別の処理
自分自身で割り込みを
起こせる命令がある
再開
割り込み発生命令
……
モード切り替え
• 何に使うかというと、モード切り替え
命令列
別の処理割り込み発生命令
……
通常のセキュリティ
レベルで動作
特権的なセキュリティ
レベルで動作
異なるモードで動作
特権モード
ユーザー モード
• 一般のプログラ
ムに認められる
セキュリティ レ
ベル
• アクセスできる
メモリに制限が
ある
特権モード※
• OSが使うセキュ
リティ レベル
• 制限がかからな
い
モード移行にはそれなりのコストが発生...
CPUの高度化
キャッシュ メモリ
マルチコアCPU
• 記憶領域の階層は多段
メイン メモリ
2次キャッシュ メモリ
キャッシュ
ALU
キャッシュ メモリ
高速
小容量
低速
大容量
速度差が大きすぎる
キャッシュ1段ごとに1桁くらい遅い
• 記憶領域の階層は多段
メイン メモリ
2次キャッシュ メモリ
キャッシュ
ALU
キャッシュ メモリ
高速
小容量
低速
大容量
このサイズに収まる範囲で
読み書きする分には高速
広範囲にデータを読み書き
すると、低速なメモリへの
読み書き...
• コアごとにキャッシュ持ってたり
メイン メモリ
2次キャッシュ メモリ
マルチコア
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
• コアで同じデータを読み書きすると
メイン メモリ
2次キャッシュ メモリ
マルチコア
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
同じブロックを読み書
きしているつもりでも 実際には別の場所にある
1...
• 非均一な読み書き速度
NUMA※
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
※ non-uniform memory access: 非均一なメモリアクセス
メモリ ノード1 メモリ ノード2 メモ...
• 非均一な読み書き速度
NUMA※
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
ALU
キャッシュ メモリ
※ non-uniform memory access: 非均一なメモリアクセス
メモリ ノード1 メモリ ノード2 メモ...
スレッド
マルチタスク
CPUのシェア
マルチタスク
• コンピューター内で複数のタスクが同時に動作
• CPUコア数に制限されない
タスク1 タスク2 タスク3 …
タスクの動作期間
実際にCPUを使って
動いている期間
1つのCPUコアを複数の
タスクがシェアしてる
問題は
• ...
2種類のマルチタスク
† preemptive: 専買権を持つ、横取りする
※cooperative
• ハードウェア タイマーを使って強制割り込み
• OSが特権的にスレッド切り替えを行う
• ○利点: 公平 (どんなタスクも等しくOSに制御...
スレッドを立てるコスト※
• スレッドに紐づいたデータ
• カーネル ステート: 1kBくらい
• ローカル スタック: 1MBくらい
• イベント発生
• Thread Attached/Detachedイベント
※ Windowsの場合
スレッド切り替えコスト
• 直接的なコスト
• 特権モードへの移行・復帰
• レジスターの保存・復元
• 次に実行するスレッドの決定
• スレッドの状態を入れ替え
• 間接的なコスト
• キャッシュ ミス
どれも性能への
インパクト大きい
スレッドは高コスト
• 細々としたタスクを大量にこなすには向かない
for (int i = 0; i < 1000; i++)
{
var t = new Thread(Worker);
t.Start();
}
大量の処理をスレッド実行
リ...
スレッド プール
• スレッドを可能な限り使いまわす仕組み
• プリエンプティブなスレッド数本の上に
• 協調的なタスク キューを用意
スレッド プール
キュー
タスク1
タスク2
…
数本※のスレッ
ドだけ用意
空いているスレッドを探して実行...
スレッド プールの性能向上
• Work Stealing Queue
• lock-free実装(後述)なローカル キュー
• できる限りスレッド切り替えが起きない作り
ローカル
キュー1
ローカル
キュー2
スレッド1 スレッド2
グローバ...
スレッド プールの性能向上
• Work Stealing Queue
• lock-free実装(後述)なローカル キュー
• できる限りスレッド切り替えが起きない作り
ローカル
キュー1
ローカル
キュー2
スレッド1 スレッド2
グローバ...
I/O完了ポート
外部ハードウェアからの応答を待つ
CPUの外の世界は遅い
• 実行速度が全然違う
ALU
メイン・メモリ
CPU 周辺機器
数千~
下手すると数万、数億倍遅い
2種類の負荷
• CPU-bound (CPUが性能を縛る)
• マルチコアCPUの性能を最大限引き出したい
• UIスレッドを止めたくない
• I/O-bound (I/O※が性能を縛る)
• ハードウェア割り込み待つだけ
• CPUは使わな...
I/O完了待ち
• I/O-boundな処理にスレッドは不要
あるスレッド
要求
応答
この間何もしないのに
スレッドを確保し続け
るのはもったいない
I/O完了ポート※
• スレッドを確保せずI/Oを待つ仕組み
• コールバックを登録して、割り込みを待つ
• コールバック処理はスレッド プールで
スレッド プール
タスク1
タスク2
…
※ I/O completion port
あるスレッ...
I/O完了ポート※
• スレッドを確保せずI/Oを待つ仕組み
• コールバックを登録して、割り込みを待つ
• コールバック処理はスレッド プールで
スレッド プール
タスク1
タスク2
…
※ I/O completion port
あるスレッ...
事例に戻って
良い例・悪い例の理由
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++)
{
...
おさらい
• Threadクラス
• Windowsの生スレッド
• = プリエンプティブなマルチタスク
• 当然重たい
• 特権モード移行、レジスター退避、…
• Taskクラス
• スレッド プールを利用
• 必要な分だけスレッド使う
推奨...
非同期I/O
using (var r = new StreamReader("some.txt"))
{
var t = r.ReadToEndAsync();
Console.WriteLine(await t);
}
using (var...
おさらい
• I/O-boundな処理のためにスレッドを専有し
ちゃダメ
• I/O完了ポート使う
• ハードウェアからの割り込みをイベント処理
• 標準ライブラリの~Async系のメソッドはこれを利
用
非推奨
Task.Run(() => ...
おまけ: SleepよりもDelay
Task.Delay((int)(x * Scale))
.ContinueWith((_, state) =>
{
sorted.Enqueue((double)state);
}, x);
var t ...
データ競合
var count = 0;
Parallel.For(0, num, i =>
{
++count;
});
var count = 0;
Parallel.For(0, num, i =>
{
lock (sync)
++cou...
おさらい
• スレッドはハードウェア タイマーの割り込み
を使ってる
• 割り込みはいつかかるかタイミング不定
• 単純なコードでも、CPUレベルでは複数命令
++count;
C# CPU命令コンパイル
① 読み mov eax, [04D7...
競合
• 複数のスレッドで同じ場所を読み書きすると…
シングル スレッド マルチ スレッド
読み
計算
書き
読み
計算
書き
読み
計算
読み
計算
読み
計算
書き
読み
計算
書き
……
……
switch
switch
書き込み終わる前...
lock
• 競合回避のためにlock (鍵)をかける
switch
switch
lock獲得
lock解放
lock獲得
獲得失敗
他のスレッドがlock獲得
しようとすると失敗する
その場でいったんスレッ
ド実行を停止
読み
計算
書き
lock
• 競合回避のためにlock (鍵)をかける
switch
lock獲得
lock解放
獲得~解放の間は、同時に2つ以上
のスレッドで実行されなくなる読み
計算
書き
lock獲得
lock解放
読み
計算
書き
lockの仕組み
• OSのスレッド スケジューラーに依頼
• lockがかかっていると、スケジューラーがスレッド
実行を止める
• 特権モードが必要
• 無駄にスレッド切り替えが増える
高コスト
switch
switch
lock獲得
lo...
スレッド ローカル
var count = 0;
Parallel.For(0, num,
() => 0,
(i, state, localCount) => localCount + 1,
localCount => count += lo...
おさらい
• lockは高コスト
• 特権モードが必要
• 無駄なスレッド切り替え発生
• スレッド間のデータ共有は高コスト
• (特に書き込み)
• キャッシュからメイン メモリへの書き戻し
• コアごとのキャッシュへの伝搬
• NUMA (...
スレッドごとの独立性
• 性能を求めるなら
• スレッド間での同じ場所の読み書きをなくす
• Parallelクラスにはそのためのオーバーロードあり
var count = 0;
Parallel.For(0, num,
() => 0,
(i...
気を付けるポイント
• 独立に計算できないと、並列化の利益少ない
• 順序依存とかがあると無理
交換法則、結合法則が大事
これが成り立たない演算は
並列化に向かない
さっきの++countループの例だと
この辺りに気を使って
アルゴリズム考える...
イベントの実装
[MethodImpl(MethodImplOptions.Synchronized)]
add { _disposed += value; }
lock (this)相当
add
{
EventHandler handler2...
おさらい
• lockは高コスト(再)
• 特権モードが必要
• 無駄なスレッド切り替え発生
• 旧実装はlock
[MethodImpl(MethodImplOptions.Synchronized)]
add { _disposed += ...
新実装
• lockなしで競合回避するためのアルゴリズム
add
{
EventHandler handler2;
var disposed = _disposed;
do
{
handler2 = disposed;
var handler3...
Interlocked
• CPUには「必ず原子的※に実行する保証付き」
な命令がいくつかある(interlocked命令)
• .NETの場合、Interlockedクラスを利用
※ atomic: 不可分な。途中で他のスレッドなどに割り込ま...
lockとinterlocked命令
• lockはOS機能
• 特権モード移行が必要
• スレッド切り替えの機会を増やす
• 任意の処理をlockできる
• Interlocked命令は単なるCPU命令
• 特権モード不要
• lockよりは...
CAS※
• 特に重要なのがCAS命令
• .NET的にはInterlocked.CompareExchangeメソッド
※ compare and swapの略。比較しながら交換
Intel CPUの命令名称的には compare excha...
CAS
• 何が重要かというと
• 競合が起きたことを検知できる
• 「競合を避ける」よりははるかに低コスト
int CompareExchange(ref int loc, int value, int comp)
{
int ret = l...
新実装がやってること
• 競合が見つかったらやりなおし
add
{
EventHandler handler2;
var disposed = _disposed;
do
{
handler2 = disposed;
var handler3 ...
注意
• この手のlock-freeアルゴリズムは書くの大変
• 書いたはいいけど、テストが大変
• 普通はライブラリまかせ
• Taskクラス(が使ってるスレッド プール)
• System.Collections.Concurrent名前空...
まとめ
• スレッドは高コスト
• スレッド プール(Taskクラス)推奨
• I/O-boundな処理にスレッド不要
• ~Asyncメソッドの利用推奨
• 競合
• lockが必要
• できればlockも避ける
• そもそもスレッド間でデー...
Upcoming SlideShare
Loading in …5
×

非同期処理の基礎

49,180 views

Published on

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

Published in: Technology

非同期処理の基礎

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

×