はじめに
マルチスレッドプログラミングでは、複数のスレッドが同じデータ(共有リソース)に同時にアクセスすると、データが破壊されたり、予期せぬ競合状態が発生したりする問題があります。これを防ぐのが、std::mutex
(ミューテックス)を使った排他制御(ロック)です。
しかし、mutex.lock()
と mutex.unlock()
を手動で管理すると、関数が途中で例外を投げたり、return
で早期に抜けたりした場合に unlock()
が呼ばれず、デッドロックに陥る危険性があります。
この問題を解決するのが、RAII (Resource Acquisition Is Initialization) というC++の原則に基づいた、std::lock_guard
と std::unique_lock
という2つのクラスです。これらのクラスは、オブジェクトが生成されたときにロックを取得し、スコープを抜けるときに自動的にロックを解放してくれます。
1. std::lock_guard
: シンプルで確実なロック管理
std::lock_guard
は、最もシンプルでオーバーヘッドの少ないロック管理クラスです。スコープベースで、基本的な排他ロック機能を提供します。
特徴
- コンストラクタで
mutex
をロックする。 - デストラクタ(スコープを抜けるとき)で
mutex
を自動的にアンロックする。 - 途中で手動アンロックしたり、ロックの所有権を移動させたりはできない。
サンプルコード
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
// 共有リソース(この例では標準出力)を保護するためのミューテックス
std::mutex mtx;
void print_message(const std::string& message) {
// lock_guardオブジェクトが生成されると同時に、mtxがロックされる
std::lock_guard<std::mutex> guard(mtx);
// このブロック内は、一度に一つのスレッドしか実行できない
std::cout << message << " : 開始" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 何か重い処理をシミュレート
std::cout << message << " : 終了" << std::endl;
// この関数の } を抜けるとき、guardのデストラクタが呼ばれ、mtxが自動的にアンロックされる
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(print_message, "スレッド " + std::to_string(i));
}
for (auto& th : threads) {
th.join();
}
return 0;
}
解説: print_message
関数では、lock_guard
がスコープを抜ける際に確実に unlock
を呼んでくれるため、lock
/unlock
の呼び忘れを心配する必要が全くありません。
2. std::unique_lock
: より柔軟なロック管理
std::unique_lock
は、lock_guard
の機能に加え、より高度で柔軟なロック管理の仕組みを提供します。
特徴
lock_guard
と同様の、スコープベースの自動ロック/アンロック。- 手動でのロック/アンロック (
.lock()
,.unlock()
) が可能。 - ロックの試行 (
.try_lock()
) が可能。 - ロックの所有権を移動(ムーブ)できる。
サンプルコード
#include <iostream>
#include <thread>
#include <mutex>
std::mutex resource_mutex;
void process_data(int id) {
// 1. ロックせずにunique_lockオブジェクトを作成 (defer_lock)
std::unique_lock<std::mutex> lock(resource_mutex, std::defer_lock);
std::cout << "スレッド " << id << " はロックを試みます..." << std::endl;
// 2. ロックを試行
if (lock.try_lock()) {
std::cout << "スレッド " << id << " はロックに成功しました。" << std::endl;
// ... 共有リソースを使った処理 ...
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// 3. 手動でアンロック(早く解放したい場合)
lock.unlock();
std::cout << "スレッド " << id << " はロックを解放しました。" << std::endl;
} else {
std::cout << "スレッド " << id << " はロックに失敗しました。" << std::endl;
}
}
int main() {
std::thread t1(process_data, 1);
std::thread t2(process_data, 2);
t1.join();
t2.join();
return 0;
}
解説: unique_lock
は、try_lock
(もしロックできたら true
、できなかったら false
を返す)のような、より複雑なロック戦略を実装する際に強力なツールとなります。
まとめ
機能 | std::lock_guard | std::unique_lock |
基本機能 | スコープベースの自動ロック/アンロック | スコープベースの自動ロック/アンロック |
シンプルさ | ◎ (非常にシンプル) | ◯ |
オーバーヘッド | ◎ (ほぼゼロ) | △ (少し大きい) |
手動ロック/アンロック | 不可 | 可能 |
ロック試行 (try_lock ) | 不可 | 可能 |
所有権の移動 | 不可 | 可能 |
結論として、単純な排他制御で十分な場合は、より軽量で安全な std::lock_guard
を使い、ロック試行や手動アンロックといった、より柔軟な制御が必要な場合にのみ std::unique_lock
を使うのがベストプラクティスです。