---
title: マルチスレッド実装を理解する ~ 正確性とパフォーマンスのバランス~
tags: 
author: [toutou node](https://www.docswell.com/user/toutou)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/5EGVZ9QQEL.jpg?width=480
description: 本発表では、マルチスレッド環境でのカウンター実装における課題を特定し、その解決策を3つのアプローチで示します。非スレッドセーフ実装では、競合状態によるデータ不整合が生じることを確認します。Lock実装では、相互排他により正確にカウントができることを示し、Lock-Free実装では高パフォーマンスを実現できることを示します。それぞれの手法の利点と懸念事項についても検討します。  おすすめタグ：マルチスレッド,カウンター,パフォーマンス,Lock,Lock-Free
published: July 05, 25
canonical: https://www.docswell.com/s/toutou/K4E388-2025-07-05-165404
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/5EGVZ9QQEL.jpg)

マルチスレッドプログラミングを理解する
正確性とパフォーマンスのバランス
発表者：toutou ( X: @__tou__tou )
※ 本資料はAI（Claude）を活用して作成されています。
技術的な内容については正確性に努めていますが、誤りが含まれる可能性があります。
1


# Page. 2

![Page Image](https://bcdn.docswell.com/page/4JQ6LWKWJP.jpg)

デモやスライドのリンク
GitHub: multi-thread-demo
Docswell: マルチスレッドプログラミングを理解する
2


# Page. 3

![Page Image](https://bcdn.docswell.com/page/K744DKP171.jpg)

概要
本発表の目的
マルチスレッド環境におけるカウンター実装の課題と解決策を検討する
対象システムの要件
秒間数千回のメソッド呼び出し
複数スレッドからの同時アクセス
システム負荷の監視指標として使用
検討する実装方式
1. 非スレッドセーフ実装
2. Lock実装
3. Lock-Free実装
3


# Page. 4

![Page Image](https://bcdn.docswell.com/page/LJ14ZX95JG.jpg)

1. 非スレッドセーフ実装
public class MethodCounter_NotThreadSafe : IMethodCounter
{
private Dictionary&lt;string, int&gt; _counts = new();
}
public void Record(string methodName)
{
if (_counts.ContainsKey(methodName))
{
// 非アトミックなRead-Modify-Write処理
// この3ステップは不可分ではないため、競合状態（Race Condition）を引き起こす。
var count = _counts[methodName]; // 1. 読み込み (Read)
count = count + 1;
// 2. 変更 (Modify)
_counts[methodName] = count;
// 3. 書き込み (Write)
}
else
{
_counts[methodName] = 1;
}
}
問題点：カウントアップがアトミック操作でないため、競合する可能性がある
ケース1：マルチコアでの並列実行
コア1のスレッドAがメモリから値 5 を読む。ほぼ同時に、コア2のスレッドBもメモリから値 5 を読む。
両方のスレッドがそれぞれ 5 + 1 を計算し、結果の 6 をメモリに書き戻そうとする。
ケース2：シングルコアでの並行実行（コンテキストスイッチ）
スレッドAがメモリから値 5 を読んだ直後、OSがスレッドBに処理を切り替える。
スレッドBが値 5 を読み、計算し、 6 を書き戻す。
その後、処理が戻ってきたスレッドAは、最初に読んだ 5 を元に計算してしまい、再度 6 を上書きしてしまう。
4


# Page. 5

![Page Image](https://bcdn.docswell.com/page/GJWX9NQWE2.jpg)

複数スレッドでカウント
static async Task Step1_ShowNotThreadSafe()
{
var counter = new MethodCounter_NotThreadSafe();
long exceptionCount = 0;
// 100スレッドを同時に起動
// Task.Run() で カウントタスク を ThreadPoolのスレッドで実行する
var tasks = Enumerable.Range(0, ThreadCount).Select(_ =&gt; Task.Run(() =&gt;
{
for (int i = 0; i &lt; CallsPerThread; i++)
{
try
{
counter.Record(&quot;TestMethod&quot;);
}
catch
{
// 例外が発生したらカウント（Dictionary破損時など）
Interlocked.Increment(ref exceptionCount);
}
}
}));
await Task.WhenAll(tasks);
}
5


# Page. 6

![Page Image](https://bcdn.docswell.com/page/4EZ693W2E3.jpg)

結果：データ不整合
テスト条件
100スレッド × 10,000回 = 合計100万回の呼び出し
発生した問題
カウント値の不整合
Dictionaryの内部構造破損による例外
6


# Page. 7

![Page Image](https://bcdn.docswell.com/page/Y762KXX6EV.jpg)

問題の原因（1/2）
メモリ構造と競合状態の発⽣メカニズム
プロセスメモリ空間
ヒープ領域（全スレッド共有）
Dictionary&lt;string, int&gt; _counts
TestMethod : 5
① Read
スレッド A
スタック
② Read
③ Write: 6
スレッド B
④ Write: 6
スタック
count = 5
count = 5
競合状態のタイムライン:
t1
Thread A
Read: 5
count = 5
t2
Thread B
Read: 5
count = 5
t3
Thread A
Modify: 5+1
Write: 6
t4
Thread B
Modify: 5+1
Write: 6
カウント消失!
7


# Page. 8

![Page Image](https://bcdn.docswell.com/page/G752PXX574.jpg)

問題の原因（2/2）
public class MethodCounter_NotThreadSafe : IMethodCounter
{
private Dictionary&lt;string, int&gt; _counts = new();
}
public void Record(string methodName)
{
if (_counts.ContainsKey(methodName))
{
// 非アトミックなRead-Modify-Write処理
// この3ステップは不可分ではないため、競合状態（Race Condition）を引き起こす。
var count = _counts[methodName]; // 1. 読み込み (Read)
count = count + 1;
// 2. 変更 (Modify)
_counts[methodName] = count;
// 3. 書き込み (Write)
}
else
{
_counts[methodName] = 1;
}
}
技術的課題
1. 共有データへの非同期アクセス
全スレッドがヒープ上の単一Dictionaryオブジェクトを共有
保護機構なしで同時読み書き
2. Read-Modify-Write操作の非アトミック性
内での3つの個別操作
操作間での割り込み発生によるカウント消失
Record()
8


# Page. 9

![Page Image](https://bcdn.docswell.com/page/9J246XXG7R.jpg)

2. Lock実装
public class MethodCounter_WithLock : IMethodCounter
{
private readonly object _lock = new();
private Dictionary&lt;string, int&gt; _counts = new();
public void Record(string methodName)
{
lock (_lock) // 一度に1スレッドのみ実行
{
if (_counts.ContainsKey(methodName))
_counts[methodName]++;
// 以下と同等
// var count = _counts[methodName]; // 1. 読み込み (Read)
// count = count + 1;
// 2. 変更 (Modify)
// _counts[methodName] = count;
// 3. 書き込み (Write)
else
_counts[methodName] = 1;
}
}
}
ポイント
カウントが正確: lockステートメント内の相互排他により、すべての操作が順序通りに実行される
9


# Page. 10

![Page Image](https://bcdn.docswell.com/page/DEYM988GEM.jpg)

マルチスレッドでLock実装の検証
static async Task Step2_ShowWithLock()
{
var counter = new MethodCounter_WithLock();
// 100スレッドを同時に起動（例外処理は不要）
var tasks = Enumerable.Range(0, ThreadCount).Select(_ =&gt; Task.Run(() =&gt;
{
for (int i = 0; i &lt; CallsPerThread; i++)
{
counter.Record(&quot;TestMethod&quot;);
}
}));
await Task.WhenAll(tasks);
// 結果を表示（カウントは必ず正確）
Console.WriteLine($&quot; -&gt; 期待したカウント: {TotalCalls:N0}&quot;);
Console.WriteLine($&quot; -&gt; 実際のカウント:
{counter.GetCountsAndReset()[&quot;TestMethod&quot;]:N0}&quot;);
// 結論：&#039;lock&#039;を使えば、安全で正確なカウンターを簡単に実装できます
}
複数スレッドで書き込み、正しくカウントされるかを確認
10


# Page. 11

![Page Image](https://bcdn.docswell.com/page/VJNWLQQ8J8.jpg)

Lock実装の結果
結果
カウント値の完全な一致
例外の発生なし
11


# Page. 12

![Page Image](https://bcdn.docswell.com/page/YE9X4DDX73.jpg)

lock の仕組み（1/2）
lock使⽤時のメモリアクセス保証
プロセスメモリ空間
ヒープ領域（全スレッド共有）
_lock オブジェクト
（同期⽤）
① ロック取得
② アクセス可能
Dictionary _counts
TestMethod : 6
❌ ブロック
スレッド A
スレッド B
ロック保持中
待機中
スタック
スタック
12


# Page. 13

![Page Image](https://bcdn.docswell.com/page/GE82Q339ED.jpg)

lock の仕組み（2/2）
lockオブジェクトの取得は CPU レベルで「アトミック操作であること」が保証されている
単一の割り込み不可能なCPU命令「Compare-And-Swap (CAS) 」で実現される
CAS (Compare-And-Swap) のアトミック動作
LOCK CMPXCHG 命令
LOCK CMPXCHG [memory_address], new_value
1. 現在のメモリ値を読み込み
2. 期待値（0）と⽐較
3. ⼀致すれば新しい値（ThreadID）を書き込み
※ これらすべてが1つの割り込み不可能な命令として実⾏
成功ケース（Thread A）
メモリ: 0
期待値: 0
新値: ThreadA
メモリ: ThreadA
成功！
失敗ケース（Thread B）
メモリ: ThreadA
期待値: 0
新値: ThreadB
メモリ: ThreadA
失敗！
13


# Page. 14

![Page Image](https://bcdn.docswell.com/page/LEL2XVVM7R.jpg)

lock取得の競合解決
マルチコア（CPU）環境では、同時に CAS 命令を発行する可能性がある
メモリコントローラーでメインメモリ上のlockオブジェクトを取得順がシリアライズされる
マルチコア環境でのCAS競合解決
CPU Core 1
CPU Core 2
CPU Core 3
CAS命令
CAS命令
CAS命令
Thread A
同時!
Thread B
同時!
Thread C
同時!
システムバス
結果: CPU1が成功
メモリコントローラー
アービトレーション
1. 要求を物理的にシリアライズ
2. 最初の要求のみを処理
メインメモリ
SyncBlock: 0 → A
14


# Page. 15

![Page Image](https://bcdn.docswell.com/page/4JM8LGG6JW.jpg)

Lock実装の懸念事項
スレッドの「lock取得の順番待ち」が
パフォーマンス低下につながる可能性
15


# Page. 16

![Page Image](https://bcdn.docswell.com/page/PJR5KZZNE9.jpg)

[参考] Lock実装
public class MethodCounter_WithLock : IMethodCounter
{
private readonly object _lock = new();
private Dictionary&lt;string, int&gt; _counts = new();
public void Record(string methodName)
{
lock (_lock) // ← スレッドが順番待ちでボトルネック
{
if (_counts.ContainsKey(methodName))
_counts[methodName]++;
else
_counts[methodName] = 1;
}
}
}
public Dictionary&lt;string, int&gt; GetCountsAndReset()
{
// 値を読み取るときに、保持しているカウントをリセットする
// このため lock が必要
lock (_lock)
{
var result = _counts;
_counts = new Dictionary&lt;string, int&gt;();
return result;
}
}
16


# Page. 17

![Page Image](https://bcdn.docswell.com/page/PEXKL998JX.jpg)

3. Lock-Free実装
ConcurrentQueue&lt;T&gt;
を使った高速化
public class MethodCounter_LockFree : IMethodCounter
{
private ConcurrentQueue&lt;string&gt; _events = new();
public void Record(string methodName)
{
_events.Enqueue(methodName); // lockなしで超高速！
// 以下と同等
//var currentQueue = _events;
// ステップ1: 参照をコピー
//_events = new ConcurrentQueue&lt;string&gt;(); // ステップ2: 新しいキューを代入
}
public Dictionary&lt;string, int&gt; GetCountsAndReset()
{
// Interlocked.Exchange は全CPUコア間で同期してアトミックなキュー交換を実行する
// GetCountsAndReset での一貫性は保証するが、Recordとの競合は防げない
var currentQueue = Interlocked.Exchange(
ref _events, new ConcurrentQueue&lt;string&gt;()
);
}
}
// Queueにたまった要素をメソッド毎に集計
var counts = new Dictionary&lt;string, int&gt;();
while (currentQueue.TryDequeue(out var methodName))
{
if (counts.ContainsKey(methodName))
counts[methodName]++;
else
counts[methodName] = 1;
}
return counts;
17


# Page. 18

![Page Image](https://bcdn.docswell.com/page/3EK5LPPLJD.jpg)

Lock実装とLock-Free実装の比較
static async Task Step3_ComparePerformance()
{
const int durationSeconds = 3;
// --- Lock版のテスト --// テスト前にGCを実行し、メモリ状態をクリーンにする
GC.Collect();
GC.WaitForPendingFinalizers();
await Task.Delay(200); // 安定待ち
var lockCounter = new MethodCounter_WithLock();
long lockWrites = await RunWriteOnlyTest(lockCounter, durationSeconds);
Console.WriteLine($&quot; -&gt;
Lock版:
{lockWrites / durationSeconds,15:N0} 件/秒&quot;);
// --- Lock-free版のテスト --GC.Collect();
GC.WaitForPendingFinalizers();
await Task.Delay(200);
var lockFreeCounter = new MethodCounter_LockFree();
long lockFreeWrites = await RunWriteOnlyTest(lockFreeCounter, durationSeconds);
Console.WriteLine($&quot; -&gt;
Lock-free版: {lockFreeWrites / durationSeconds,15:N0} 件/秒&quot;);
}
double speedup = (double)lockFreeWrites / lockWrites;
Console.WriteLine($&quot;\n [結論] Lock-Free版はLock版の約{speedup:F2}倍高速！&quot;);
// RunWriteOnlyTestヘルパーメソッド
static async Task&lt;long&gt; RunWriteOnlyTest(IMethodCounter counter, int durationSeconds)
{
long totalWritten = 0;
var cts = new CancellationTokenSource();
var keys = Enumerable.Range(0, 100).Select(i =&gt; $&quot;Method_{i}&quot;).ToArray();
var writerTasks = Enumerable.Range(0, 100)
.Select(threadIndex =&gt; Task.Run(() =&gt;
{
var random = new Random(threadIndex);
while (!cts.IsCancellationRequested)
{
counter.Record(keys[random.Next(keys.Length)]);
Interlocked.Increment(ref totalWritten);
}
})).ToList();
}
await Task.Delay(TimeSpan.FromSeconds(durationSeconds));
cts.Cancel();
await Task.WhenAll(writerTasks);
return totalWritten;
18


# Page. 19

![Page Image](https://bcdn.docswell.com/page/L73K3XX675.jpg)

Lock実装とLock-Free実装の性能比較 [Windows環境]
Thread待機時間が減ってパフォーマンスが6倍向上
19


# Page. 20

![Page Image](https://bcdn.docswell.com/page/87D3422MJG.jpg)

Lock実装とLock-Free実装の性能比較 [WSL環境]
Thread待機時間が減ってパフォーマンスが1.6倍向上
20


# Page. 21

![Page Image](https://bcdn.docswell.com/page/VJP4M11Q78.jpg)

Lock-Free実装の課題: 大量の同時読み書きシナリオ
static async Task Step4_ShowLockFreeDataLoss()
{
var counter = new MethodCounter_LockFree();
//var counter = new MethodCounter_WithLock(); // Lock版
long totalWritten = 0;
long totalRead = 0;
var cts = new CancellationTokenSource();
// 大量書き込み
var writerTasks = Enumerable.Range(0, ThreadCount).Select(_ =&gt; Task.Run(() =&gt;
{
while (!cts.IsCancellationRequested)
{
counter.Record(&quot;Event&quot;);
Interlocked.Increment(ref totalWritten);
}
})).ToList(); // タスクを開始させる（Enumerableのままだと遅延実行になってしまう）
// 大量読み出し（リセットによる書き込みも含む）
var readerTask = Task.Run(async () =&gt;
{
while (!cts.IsCancellationRequested)
{
var c = counter.GetCountsAndReset();
Interlocked.Add(ref totalRead, c.Values.Sum());
await Task.Delay(1);
}
});
await Task.Delay(10000); // 10秒間テスト
cts.Cancel();
await Task.WhenAll(writerTasks.Append(readerTask));
totalRead += counter.GetCountsAndReset().Values.Sum(); // 残りを回収
}
Console.WriteLine($&quot; -&gt; 書き込み総数: {totalWritten:N0}&quot;);
Console.WriteLine($&quot; -&gt; 読み取り総数: {totalRead:N0}&quot;);
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($&quot; -&gt; ロストした数: {totalWritten - totalRead:N0}&quot;);
Console.WriteLine(&quot;\n [結論] わずかですが、データがロストしてしまいました！\n&quot;);
Console.ResetColor();
21


# Page. 22

![Page Image](https://bcdn.docswell.com/page/2EVX9YYPJQ.jpg)

Lock-Free実装の大量同時書き込み結果
結果
わずかにデータロスト発生
22


# Page. 23

![Page Image](https://bcdn.docswell.com/page/57GVZ99Q7L.jpg)

データロストの理由
Record() と GetCountsAndReset() の競合
Record( MethodC ) - Thread A
GetCountsAndReset() - Thread B
public void Record(string methodName)
{
public Dictionary GetCountsAndReset()
{
// ① 参照を取得
var queue = this._events;
// ② アトミックに交換
var oldQueue = Interlocked.Exchange(
ref _events, new ConcurrentQueue());
// ② ここで割り込み！
// 集計処理...
// ③ 古いキューに追加
queue.Enqueue(methodName);
}
return counts;
}
メモリ状態の変化
初期状態
_events → Queue1
Queue1: [A, B]
① 参照取得
② Exchange実⾏
③ Enqueue実⾏
Queue1: [A, B]
Queue1: [A, B]
Queue2: []
Queue1: [A, B, C]
Queue2: []
_events → Queue1
queue = Queue1
_events → Queue2
queue = Queue1
_events → Queue2
queue = Queue1
結果
GetCountsAndReset() は Queue1 を集計後に破棄
MethodC は集計に含まれずデータロスト！
Interlocked.ExchangeはGetCountsAndResetの内部的な⼀貫性は保証するが、、Recordメソッドとの競合は防げない
23


# Page. 24

![Page Image](https://bcdn.docswell.com/page/4EQ6LWWWEP.jpg)

まとめ：3つの実装の比較
実装方式
NotThreadSafe
WithLock
正確性
LockFree
性能 適用場面
－ なし
正確性が最優先
（課金、在庫管理とか）
高速性が重要
（アクセス解析、ログとか）
要件に応じて実装を使い分けよう
24


# Page. 25

![Page Image](https://bcdn.docswell.com/page/KJ44DKK1J1.jpg)

その他・検証したかったこと
メモリバリアとvolatile
Read Barrier: 読み込み順序保証
Write Barrier: 書き込み順序保証
Full Barrier: 両方保証（lock, Interlocked）
volatile は順序保証のみ、アトミック性は保証しない
同期プリミティブの使い分け
lock: 一般的な相互排他
ReaderWriterLockSlim: 読み取り頻度が &gt;&gt; 書き込み頻度 の場合
SemaphoreSlim: 同時アクセス数制限
パフォーマンス考慮したlockの使い分け
読み書き比率: 7:1以上なら ReaderWriterLockSlim
待機時間: 短時間=SpinLock、長時間=通常lock
スケーラビリティ: 高負荷時はlock-freeデータ構造
ベンチマーク実装に BenchmarkDotNet を使う
25


# Page. 26

![Page Image](https://bcdn.docswell.com/page/LE14ZXX5EG.jpg)

リンク
GitHub: multi-thread-demo
X: @__tou__tou
26


