スレッドの同期
[第]43[課では]マルチスレッド[の基礎]を学びました。
[現在]、[私たちは]マルチスレッドの同期について深く掘り下げていきます――[複数の]スレッド[が協調して動作する]ようにし、[競合や]デッドロックを回避します。
マルチスレッド[プログラミングの難しさは、スレッドの作成にあるのではなく]、[むしろ]同期処理にある。
1. デッドロック
(1) 1.1 デッドロックとは何か?
デッドロック(Deadlock)[とは、2つ以上の]スレッド[が互いに][相手がリソースを解放するのを待っている]状態を指し、[その結果、いずれも処理を続行できなくなる]。
[4つの必要]条件:
- [排他]:[リソースは共有できない]
- [保持して待機]:[リソースを保持している]スレッド[が他のリソースを待機している]
- [非プリエンプティブ]:[リソースは強制的に解放されない]
- ループ[待機]:[存在する]スレッド[の]ループ[待機チェーン]
(2) 1.2 デッドロックの例
例:デッドロック(難易度 ⭐⭐⭐)
▶ サンプル 2: マルチスレッド[プログラミングデモ] (難易度 ⭐)
#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:[ロック順序の固定]
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 を使用して複数のミューテックスを同時にロックする]
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、[推奨])
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_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テンプレートが[提供されています]。
例:[原子カウンター] (難易度 ⭐⭐)
#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)、[同期の]範囲[を制御するために使用される]。
[常用]メモリ[順序]:
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::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: [用]マルチスレッド[によるソートの高速化]([難易度]⭐⭐⭐⭐)
#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;
}
❓ よくある質問
std::cout [ログを出力する]([ただし、ミューテックスを追加する必要がある]) - [Valgrind、Helgrind] [などのツールを使用してデータ競合を検出する] - [Thread Sanitizer(GCC/Clang)]std::jthread([自動]join) - [シグナル](std::counting_semaphore) - [ラッチ](std::latch) - [バリア](std::barrier)📖 まとめ
| [知識ポイント] | [要点] |
|---|---|
| デッドロック | [4つの]条件、[3つの回避策] |
| [読み書きロック] | std::shared_mutex(C++17) |
| [原子操作] | std::atomic、[ロック不要] |
| スレッド[プール] | [スレッドの再利用],[オーバーヘッドの削減] |
| [並列]アルゴリズム | C++17 std::execution |
📝 練習問題
-
**初心者(難易度 ⭐):[2つの]スレッド[を同時に作成し、1つの]グローバル変数[に対して] 100000 [回のインクリメント]を行い、[結果が正しいかどうかを確認]する。[その後、] std::mutex [で保護してから再度テスト]する。
-
**中級(難易度 ⭐⭐):[std::lock_guard を使用して]「[口座残高]」変数を保護し、[2つのスレッドをシミュレートして][同時に入金]/[出金操作]を行い、[最終的な残高が正しいことを保証する]。
-
**上級(難易度 ⭐⭐⭐):[std::condition_variable を使用して]「[プロデューサー]-[コンシューマー]」モデルを実装してください。[1つの]スレッドが[データをキューに格納し]、[もう1つの]スレッドが[データを消費します]。[キューが空のときは、コンシューマーは待機します]。
- [競合]条件:[複数の]スレッドが[共有データに同時にアクセスする]
- std::mutex mutex[クリティカルセクションの保護]
- std::lock_guard [自動ロック・アンロック](RAII)
- デッドロック:[2つの]スレッド[が互いに相手がロックを解放するのを待っている状態]
- 条件変数 std::condition_variable [通知]スレッド
フェーズ6 [終了]![次へ]:フェーズ7([応用と]練習)



