C++: スレッド同期

最終更新:2026-08-31

レッスン43では、マルチスレッド基本について学びました。

ここでは、マルチスレッド同期について学びます——スレッド間のデータ競合を防ぎ、デッドロックを回避します。

マルチスレッドプログラミングで最も重要なのが、スレッド間の同期です。


1. デッドロック

(1) 1.1 デッドロックとは?

デッドロック(Deadlock)は複数スレッドが互いに待ち合い、どちらも進めなくなる状態です。

発生条件:

  1. 相互排他:リソースは1つのスレッドのみが使用可能
  2. 保持と待機:あるスレッドがリソースを保持しながら他を待つ
  3. 横取り不可:リソースを強制的に奪えない
  4. 循環待機:スレッド間で循環的な待機関係

(2) 1.2 デッドロックの例

▶ サンプル 2:マルチスレッドプログラミング(難易度 ⭐)

TEXT 📖 参照専用
#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:ロック順序の統一

CPP
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で一括ロック

TEXT 📖 参照専用
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、推奨)

CPP
void safe_function() {
    std::scoped_lock lock(mtx1, mtx2); // 複数ミューテックスを安全にロック
    
    // 処理...
}



2. 読み書きロック

(1) 2.1 なぜ読み書きロックが必要?

問題: ミューテックスは「排他」——読み取り専用のスレッドでも待たなければならない

解決策: 読み書きロック(std::shared_mutex、C++17)

タイプ 機能
読み取りロック 複数スレッドが同時に読み取り可能
書き込みロック 排他、他スレッドは読み取りも不可

(2) 2.2 例:読み書きロックでキャッシュ(難易度 ⭐⭐⭐)

CPP
#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 テンプレートが導入されました。

▶ サンプル:アトミックカウンタ(難易度 ⭐⭐)

CPP
#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)を指定でき、同期の範囲を調整できます。

主なメモリオーダー:




4. スレッドプール

(1) 4.1 なぜスレッドプールが必要?

問題: スレッド作成/破棄のコストが高い、無制限なスレッド作成はリソースを枯渇させる。

解決策: スレッドプール——固定数のスレッドでタスクを処理、再利用。


(2) 4.2 スレッドプールの実装

▶ サンプル:スレッドプール(難易度 ⭐⭐⭐⭐)

TEXT 📖 参照専用
#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:マルチスレッドソート(難易度 ⭐⭐⭐⭐)

CPP
#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:スレッドセーフなカウンタ(難易度 ⭐⭐)

CPP
#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

📝 練習問題

  1. 初級(難易度 ⭐): 複数スレッドでグローバル変数を100000回インクリメント、結果を観察。次に std::mutex を使用してテスト。

  2. 中級(難易度 ⭐⭐): std::lock_guard で「スレッドセーフな」変数を実装、複数スレッドで読み取り/書き込み。

  3. 上級(難易度 ⭐⭐⭐): std::condition_variable で「プロデューサー・コンシューマー」を実装。プロデューサースレッドがキューにデータを追加、コンシューマースレッドがキューからデータを取り出して処理。



フェーズ6終了!フェーズ7(応用実践)へ

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%