C++: スレッド同期
最終更新:2026-08-31
レッスン43では、マルチスレッド基本について学びました。
ここでは、マルチスレッド同期について学びます——スレッド間のデータ競合を防ぎ、デッドロックを回避します。
マルチスレッドプログラミングで最も重要なのが、スレッド間の同期です。
1. デッドロック
(1) 1.1 デッドロックとは?
デッドロック(Deadlock)は複数スレッドが互いに待ち合い、どちらも進めなくなる状態です。
発生条件:
- 相互排他:リソースは1つのスレッドのみが使用可能
- 保持と待機:あるスレッドがリソースを保持しながら他を待つ
- 横取り不可:リソースを強制的に奪えない
- 循環待機:スレッド間で循環的な待機関係
(2) 1.2 デッドロックの例
▶ サンプル 2:マルチスレッドプログラミング(難易度 ⭐)
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(mtx2); // mtx2を待つ
std::cout << "Thread 1 done" << std::endl;
}
void thread2() {
std::lock_guard<std::mutex> lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock1(mtx1); // mtx1を待つ
std::cout << "Thread 2 done" << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
- このプログラムは永遠に終わらない可能性があります(デッドロック)
(3) 1.3 デッドロックの回避
方法1:ロック順序の統一
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::lock_guard<std::mutex> lock2(mtx2); // mtx1→mtx2の順序
std::cout << "Thread 1 done" << std::endl;
}
void thread2() {
std::lock_guard<std::mutex> lock1(mtx1); // thread1と同じ順序
std::lock_guard<std::mutex> lock2(mtx2);
std::cout << "Thread 2 done" << std::endl;
}
方法2:std::lockで一括ロック
void safe_function() {
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
std::lock(lock1, lock2); // デッドロック回避で一括ロック
// 処理...
}
方法3:std::scoped_lockを使用(C++17、推奨)
void safe_function() {
std::scoped_lock lock(mtx1, mtx2); // 複数ミューテックスを安全にロック
// 処理...
}
2. 読み書きロック
(1) 2.1 なぜ読み書きロックが必要?
問題: ミューテックスは「排他」——読み取り専用のスレッドでも待たなければならない。
解決策: 読み書きロック(std::shared_mutex、C++17)
| タイプ | 機能 |
|---|---|
| 読み取りロック | 複数スレッドが同時に読み取り可能 |
| 書き込みロック | 排他、他スレッドは読み取りも不可 |
(2) 2.2 例:読み書きロックでキャッシュ(難易度 ⭐⭐⭐)
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <unordered_map>
std::unordered_map<int, int> cache;
std::shared_mutex smtx;
int get(int key) {
std::shared_lock<std::shared_mutex> lock(smtx); // 読み取りロック(共有)
auto it = cache.find(key);
if (it != cache.end()) {
return it->second;
}
return -1;
}
void set(int key, int value) {
std::unique_lock<std::shared_mutex> lock(smtx); // 書き込みロック(排他)
cache[key] = value;
}
int main() {
// 使用例
return 0;
}
3. アトミック操作
(1) 3.1 アトミックとは?
アトミック操作(Atomic Operation)は分割不可能な操作——途中で中断されず、他スレッドからは完了した状態しか見えません。
利点: ミューテックス不要、パフォーマンス向上。
(2) 3.2 std::atomic
C++11で std::atomic テンプレートが導入されました。
▶ サンプル:アトミックカウンタ(難易度 ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
std::atomic<int> counter(0); // アトミック変数
void increment() {
for (int i = 0; i < 1000; i++) {
counter++; // ロック不要、安全にインクリメント
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << counter << std::endl; // 10000
return 0;
}
(3) 3.3 メモリオーダー
std::atomic はメモリオーダー(Memory Order)を指定でき、同期の範囲を調整できます。
主なメモリオーダー:
std::memory_order_relaxed:順序保証なしstd::memory_order_acquire:読み取り、以降の操作が再順序されないstd::memory_order_release:書き込み、以前の操作が再順序されないstd::memory_order_seq_cst:最強の順序保証(デフォルト)
4. スレッドプール
(1) 4.1 なぜスレッドプールが必要?
問題: スレッド作成/破棄のコストが高い、無制限なスレッド作成はリソースを枯渇させる。
解決策: スレッドプール——固定数のスレッドでタスクを処理、再利用。
(2) 4.2 スレッドプールの実装
▶ サンプル:スレッドプール(難易度 ⭐⭐⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop;
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; i++) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] {
return stop || !tasks.empty();
});
if (stop && tasks.empty()) {
return;
}
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& t : workers) {
t.join();
}
}
template<class F>
void enqueue(F&& f) {
{
std::lock_guard<std::mutex> lock(mtx);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
};
int main() {
ThreadPool pool(4); // 4スレッド
for (int i = 0; i < 10; i++) {
pool.enqueue([i] {
std::cout << "Task " << i << " running" << std::endl;
});
}
return 0;
}
5. 実践:並列ソート
▶ サンプル 1:マルチスレッドソート(難易度 ⭐⭐⭐⭐)
#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>
void parallelSort(std::vector<int>& v, int left, int right) {
if (left >= right) return;
std::sort(v.begin() + left, v.begin() + right + 1);
}
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// 2分割で並列ソート
std::thread t1(parallelSort, std::ref(v), 0, v.size()/2);
std::thread t2(parallelSort, std::ref(v), v.size()/2 + 1, v.size()-1);
t1.join();
t2.join();
// 結合
std::inplace_merge(v.begin(), v.begin() + v.size()/2 + 1, v.end());
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
▶ サンプル 3:スレッドセーフなカウンタ(難易度 ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
class Counter {
private:
int value;
std::mutex mtx;
public:
Counter() : value(0) {}
void increment() {
std::lock_guard<std::mutex> lock(mtx);
value++;
}
int get() const { return value; }
};
int main() {
Counter counter;
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back([&counter] {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
}
for (auto& t : threads) {
t.join();
}
std::cout << "最終カウント: " << counter.get() << std::endl;
return 0;
}
出力:
TEXT 📖 参照専用最終カウント: 10000
❓ よくある質問
Q:マルチスレッドのデバッグは難しい? A: はい。競合状態は再現が難しく、スレッド間の同期が重要です。
Q:マルチスレッドプログラムのデバッグ方法は? A: -
std::coutでログ出力(ただしミューテックス必要)
- Valgrind、Helgrindなどのツール使用
- Thread Sanitizer使用(GCC/Clang)
Q:C++20の新機能は? A: -
std::jthread(自動join)
- セマフォ(
std::counting_semaphore)- ラッチ(
std::latch)- バリア(
std::barrier)
📖 まとめ
| トピック | 要点 |
|---|---|
| デッドロック | 発生条件、回避方法 |
| 読み書きロック | std::shared_mutex(C++17) |
| アトミック操作 | std::atomic、ロック不要 |
| スレッドプール | 固定スレッドでタスク処理、効率的 |
| 並列アルゴリズム | C++17 std::execution |
📝 練習問題
-
初級(難易度 ⭐): 複数スレッドでグローバル変数を100000回インクリメント、結果を観察。次に
std::mutexを使用してテスト。 -
中級(難易度 ⭐⭐):
std::lock_guardで「スレッドセーフな」変数を実装、複数スレッドで読み取り/書き込み。 -
上級(難易度 ⭐⭐⭐):
std::condition_variableで「プロデューサー・コンシューマー」を実装。プロデューサースレッドがキューにデータを追加、コンシューマースレッドがキューからデータを取り出して処理。
- 競合状態:複数スレッドが同時に同じデータにアクセス
std::mutexでミューテックスを実現std::lock_guardで自動ロック(RAII)- デッドロック:スレッド同士が互いに待ち合う
- 条件変数
std::condition_variableでスレッド間通信
フェーズ6終了!フェーズ7(応用実践)へ