std::mutex / std::lock_guard
マルチスレッドプログラムでは、複数のスレッドが同じデータに同時にアクセスすると、競合状態(race condition)が発生してデータが破損するおそれがあります。『C++』では std::mutex と std::lock_guard を使って排他制御(mutual exclusion)を実現できます。lock_guard はスコープを抜けると自動的にロックを解放するため、例外が発生した場合でもデッドロックが起きません。
構文
// ========================================
// std::mutex と std::lock_guard の基本構文
// ========================================
#include <mutex>
#include <thread>
std::mutex mtx; // 保護したいリソースに対応するミューテックスを用意します
void criticalSection() {
// lock_guard のコンストラクタでロックを取得します
// スコープを抜けると自動的に unlock() が呼ばれます
std::lock_guard<std::mutex> lock(mtx);
// --- この範囲が排他制御されます ---
// 同時に1スレッドだけが実行できます
} // ここで自動的にロック解放されます
// mtx.lock() / mtx.unlock() を手動で呼ぶ方法(手動管理が必要な方法です)
// 例外が起きると unlock() が呼ばれずデッドロックになります
void manualLock() {
mtx.lock();
// 注意:ここで例外が発生すると unlock() が呼ばれません
mtx.unlock();
}
// unique_lock:ロックの遅延取得や途中解放が必要な場合に使います
void withUniqueLock() {
std::unique_lock<std::mutex> lock(mtx);
// lock.unlock() で途中解放、lock.lock() で再取得できます
}
構文一覧
| 構文・クラス | 概要 |
|---|---|
| std::mutex | 最も基本的なミューテックスです。lock() と unlock() でスレッド間の排他制御を行います。 |
| std::lock_guard<std::mutex> | コンストラクタでロックを取得し、デストラクタで自動解放するRAIIラッパーです。例外安全な排他制御に使います。 |
| std::unique_lock<std::mutex> | lock_guard より柔軟なRAIIラッパーです。途中でのロック解放・再取得や、条件変数との組み合わせが必要な場合に使います。 |
| mtx.lock() | ミューテックスを手動でロックします。すでに他スレッドがロック中の場合はブロックして待機します。 |
| mtx.unlock() | ミューテックスを手動で解放します。lock_guard を使う場合は呼ぶ必要がありません。 |
| std::lock(m1, m2) | 複数のミューテックスをデッドロックなしに同時にロックします。 |
| std::recursive_mutex | 同一スレッドから複数回ロックできるミューテックスです。再帰呼び出しを含む関数の排他制御に使います。 |
サンプルコード
deathnote_mutex.cpp
// ========================================
// deathnote_mutex.cpp
// DEATH NOTE の捜査事件データベースに
// 複数の捜査員スレッドが同時書き込みを行う場面で
// std::mutex と lock_guard による排他制御を示すサンプルです
// ========================================
#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <map>
// ========================================
// CaseDatabase:捜査事件データベースです
// 複数スレッドから安全に読み書きするため mutex で保護します
// ========================================
class CaseDatabase {
private:
std::map<std::string, int> suspectPriority; // 容疑者名 → 優先度のマップです
std::mutex mtx; // データ保護用のミューテックスです
std::vector<std::string> log; // 更新ログです
public:
// 容疑者の捜査優先度を更新します(スレッドセーフ)
void update(const std::string& suspect, int priority, const std::string& investigator) {
// lock_guard を生成した瞬間にロックを取得します
// このスコープを抜けると(return・例外どちらでも)自動解放されます
std::lock_guard<std::mutex> lock(mtx);
suspectPriority[suspect] = priority;
std::string entry = "[" + investigator + "] " + suspect
+ " の捜査優先度を " + std::to_string(priority) + " に更新しました。";
log.push_back(entry);
std::cout << entry << std::endl;
} // ここで lock が破棄され、mtx.unlock() が自動で呼ばれます
// 捜査優先度を照会します(スレッドセーフ)
int query(const std::string& suspect) {
std::lock_guard<std::mutex> lock(mtx);
auto it = suspectPriority.find(suspect);
if (it != suspectPriority.end()) {
return it->second;
}
return -1; // データなしを示します
}
// 全ログを出力します(呼び出し側でロック不要な完了後の確認用です)
void printLog() {
std::lock_guard<std::mutex> lock(mtx);
std::cout << std::endl << "=== 更新ログ(全 " << log.size() << " 件) ===" << std::endl;
for (int i = 0; i < (int)log.size(); i++) {
std::cout << " " << log[i] << std::endl;
}
}
};
// ========================================
// 捜査員スレッドのタスクです
// 担当する容疑者の捜査優先度を評価してデータベースに書き込みます
// ========================================
void investigatorTask(CaseDatabase& db,
const std::string& investigator,
const std::string& suspect,
int priority) {
// 複数スレッドが同時にここに到達してもデータ競合が起きません
db.update(suspect, priority, investigator);
}
int main() {
CaseDatabase db;
std::cout << "=== DEATH NOTE 捜査事件管理システム ===" << std::endl;
std::cout << "複数の捜査員スレッドが同時に優先度を更新します。" << std::endl << std::endl;
// ----------------------------------------
// 各捜査員が別スレッドで容疑者の捜査優先度を登録します
// mutex がないと suspectPriority の書き込みが競合してデータが壊れます
// ----------------------------------------
std::thread t1(investigatorTask, std::ref(db), "L", "夜神月", 95);
std::thread t2(investigatorTask, std::ref(db), "ニア", "夜神月", 98);
std::thread t3(investigatorTask, std::ref(db), "メロ", "弥海砂", 72);
std::thread t4(investigatorTask, std::ref(db), "L", "弥海砂", 68);
// 全スレッドの完了を待ちます
t1.join();
t2.join();
t3.join();
t4.join();
std::cout << std::endl << "=== 最終照会結果 ===" << std::endl;
// ----------------------------------------
// スレッド完了後、メインスレッドから最終優先度を確認します
// query() も lock_guard で保護されているため自動的にロック解除されます
// ----------------------------------------
std::vector<std::string> suspects = { "夜神月", "弥海砂" };
for (int i = 0; i < (int)suspects.size(); i++) {
int priority = db.query(suspects[i]);
std::cout << suspects[i] << " の最終捜査優先度: " << priority << std::endl;
}
db.printLog();
return 0;
}
# コンパイルします(-lpthread でスレッドライブラリをリンクします) g++ -std=c++11 deathnote_mutex.cpp -o deathnote_mutex -lpthread # 実行します ./deathnote_mutex === DEATH NOTE 捜査事件管理システム === 複数の捜査員スレッドが同時に優先度を更新します。 [L] 夜神月 の捜査優先度を 95 に更新しました。 [ニア] 夜神月 の捜査優先度を 98 に更新しました。 [メロ] 弥海砂 の捜査優先度を 72 に更新しました。 [L] 弥海砂 の捜査優先度を 68 に更新しました。 === 最終照会結果 === 夜神月 の最終捜査優先度: 98 弥海砂 の最終捜査優先度: 68 === 更新ログ(全 4 件) === [L] 夜神月 の捜査優先度を 95 に更新しました。 [ニア] 夜神月 の捜査優先度を 98 に更新しました。 [メロ] 弥海砂 の捜査優先度を 72 に更新しました。 [L] 弥海砂 の捜査優先度を 68 に更新しました。
よくあるミス: lock_guard を使わず手動で lock/unlock を書く
手動で mtx.lock() / mtx.unlock() を書くと、間に例外が発生したときに unlock() が呼ばれずデッドロックになります。
// NG: 手動で lock/unlock を書く方法です
void updateManual(std::mutex& mtx, std::map<std::string, int>& db,
const std::string& name, int val) {
mtx.lock();
// ここで例外が発生すると unlock() が呼ばれずデッドロックになります
db[name] = val;
mtx.unlock();
}
OK: lock_guard を使えばスコープを抜けたときに自動的にロック解除されます。
// OK: lock_guard を使います
void updateSafe(std::mutex& mtx, std::map<std::string, int>& db,
const std::string& name, int val) {
std::lock_guard<std::mutex> lock(mtx); // 自動的にロック解除されます
db[name] = val;
} // スコープを抜けると lock が破棄されて unlock() が自動で呼ばれます
よくあるミス: 同一スレッドから std::mutex を二重にロックする
同じスレッドが同じ std::mutex を二重にロックしようとするとデッドロックになります。再帰的なロックが必要な場面では std::recursive_mutex を使います。
// NG: 同一スレッドが同じ mutex を二重ロックするとデッドロックになります
std::mutex mtx;
void outerFunc() {
std::lock_guard<std::mutex> lock(mtx);
innerFunc(); // innerFunc() 内でも mtx をロックしようとすると停止します
}
void innerFunc() {
std::lock_guard<std::mutex> lock(mtx); // デッドロック発生
}
OK: 再帰的なロックが必要な場合は std::recursive_mutex を使います。
// OK: recursive_mutex を使えば同一スレッドから複数回ロックできます
std::recursive_mutex rmtx;
void outerFunc() {
std::lock_guard<std::recursive_mutex> lock(rmtx);
innerFunc(); // 同一スレッドからの再ロックが可能です
}
void innerFunc() {
std::lock_guard<std::recursive_mutex> lock(rmtx); // OK
}
概要
std::mutex はスレッド間の排他制御を実現する最も基本的な同期プリミティブです。lock() を呼んだスレッドだけがクリティカルセクションを実行でき、他のスレッドは unlock() が呼ばれるまでブロックして待機します。ただし、手動で lock() / unlock() を書くと、例外が発生したときに unlock() が呼ばれずデッドロックになる危険があります。これを防ぐために std::lock_guard を使います。lock_guard はコンストラクタでロックを取得し、スコープを抜けたときにデストラクタで自動的に解放するRAIIラッパーで、例外が発生した場合でも確実に解放されます。より柔軟な制御(ロックの途中解放・再取得、条件変数との組み合わせ)が必要な場合は std::unique_lock を使います。なお、同じスレッドから同じ std::mutex を二重にロックするとデッドロックになるため、再帰的なロックが必要な場合は std::recursive_mutex を使ってください。std::mutex および関連クラスは <mutex> ヘッダーに、std::thread は <thread> ヘッダーに定義されています。コンパイル時には -lpthread オプションが必要です。
関連項目: thread / condition_variable
記事の間違いや著作権の侵害等ございましたらお手数ですがこちらまでご連絡頂ければ幸いです。