404 Not Found

404 Not Found


nginx

スレッドの同期

[第]43[課では]マルチスレッド[の基礎]を学びました。

[現在]、[私たちは]マルチスレッドの同期について深く掘り下げていきます――[複数の]スレッド[が協調して動作する]ようにし、[競合や]デッドロックを回避します。

マルチスレッド[プログラミングの難しさは、スレッドの作成にあるのではなく]、[むしろ]同期処理にある。


1. デッドロック

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

デッドロック(Deadlock)[とは、2つ以上の]スレッド[が互いに][相手がリソースを解放するのを待っている]状態を指し、[その結果、いずれも処理を続行できなくなる]。

[4つの必要]条件:

  1. [排他]:[リソースは共有できない]
  2. [保持して待機]:[リソースを保持している]スレッド[が他のリソースを待機している]
  3. [非プリエンプティブ]:[リソースは強制的に解放されない]
  4. ループ[待機]:[存在する]スレッド[の]ループ[待機チェーン]

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

例:デッドロック(難易度 ⭐⭐⭐)

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

CPP
#include iostream
#include thread
#include mutex

std::mutex mtx1, mtx2;

void thread1() {
 std::lock_guardstd::mutex lock1(mtx1);
 std::this_thread::sleep_for(std::chrono::milliseconds(100));
 std::lock_guardstd::mutex lock2(mtx2); // mtx2
 std::cout << "Thread 1 done" << std::endl;
}

void thread2() {
 std::lock_guardstd::mutex lock2(mtx2);
 std::this_thread::sleep_for(std::chrono::milliseconds(100));
 std::lock_guardstd::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_guardstd::mutex lock1(mtx1);
 std::lock_guardstd::mutex lock2(mtx2); // mtx1,mtx2
 std::cout << "Thread 1 done" << std::endl;
}

void thread2() {
 std::lock_guardstd::mutex lock1(mtx1); // thread1
 std::lock_guardstd::mutex lock2(mtx2);
 std::cout << "Thread 2 done" << std::endl;
}

[方法]2:[std::lock を使用して複数のミューテックスを同時にロックする]

CPP
void safe_function() {
 std::unique_lockstd::mutex lock1(mtx1, std::defer_lock);
 std::unique_lockstd::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_lockstd::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_lockstd::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::atomicint counter(0); // ...

void increment() {
 for (int i = 0; i < 1000; i++) {
 counter++; //,
 }
}

int main() {
 std::vectorstd::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 [簡単]スレッド[プール実装]

例:スレッド[池]([難易度]⭐⭐⭐⭐)

CPP
#include iostream
#include thread
#include vector
#include queue
#include functional
#include mutex
#include condition_variable

class ThreadPool {
private:
 std::vectorstd::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_lockstd::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_guardstd::mutex lock(mtx);
 stop = true;
 }
 cv.notify_all();
 for (auto& t : workers) {
 t.join();
 }
 }
 
 template<class F>
 void enqueue(F&& f) {
 {
 std::lock_guardstd::mutex lock(mtx);
 tasks.emplace(std::forwardF(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::vectorint& v, int left, int right) {
 if (left >= right) return;
 
 std::sort(v.begin() + left, v.begin() + right + 1);
}

int main() {
 std::vectorint 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();
 
 // points
 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;
}
▶ 試してみよう

❓ よくある質問

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

📖 まとめ

[知識ポイント] [要点]
デッドロック [4つの]条件、[3つの回避策]
[読み書きロック] std::shared_mutex(C++17)
[原子操作] std::atomic、[ロック不要]
スレッド[プール] [スレッドの再利用],[オーバーヘッドの削減]
[並列]アルゴリズム C++17 std::execution

📝 練習問題

  1. **初心者(難易度 ⭐):[2つの]スレッド[を同時に作成し、1つの]グローバル変数[に対して] 100000 [回のインクリメント]を行い、[結果が正しいかどうかを確認]する。[その後、] std::mutex [で保護してから再度テスト]する。

  2. **中級(難易度 ⭐⭐):[std::lock_guard を使用して]「[口座残高]」変数を保護し、[2つのスレッドをシミュレートして][同時に入金]/[出金操作]を行い、[最終的な残高が正しいことを保証する]。

  3. **上級(難易度 ⭐⭐⭐):[std::condition_variable を使用して]「[プロデューサー]-[コンシューマー]」モデルを実装してください。[1つの]スレッドが[データをキューに格納し]、[もう1つの]スレッドが[データを消費します]。[キューが空のときは、コンシューマーは待機します]。



フェーズ6 [終了]![次へ]:フェーズ7([応用と]練習)

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%