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

More Related Content

非同期処理の基礎