【C++】std::lock_guardとstd::unique_lockの使い方 | mutexを安全に管理する方法

目次

はじめに

マルチスレッドプログラミングでは、複数のスレッドが同じデータ(共有リソース)に同時にアクセスすると、データが破壊されたり、予期せぬ競合状態が発生したりする問題があります。これを防ぐのが、std::mutex(ミューテックス)を使った排他制御(ロック)です。

しかし、mutex.lock()mutex.unlock() を手動で管理すると、関数が途中で例外を投げたり、returnで早期に抜けたりした場合に unlock() が呼ばれず、デッドロックに陥る危険性があります。

この問題を解決するのが、RAII (Resource Acquisition Is Initialization) というC++の原則に基づいた、std::lock_guardstd::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_guardstd::unique_lock
基本機能スコープベースの自動ロック/アンロックスコープベースの自動ロック/アンロック
シンプルさ (非常にシンプル)
オーバーヘッド (ほぼゼロ)△ (少し大きい)
手動ロック/アンロック不可可能
ロック試行 (try_lock)不可可能
所有権の移動不可可能

結論として、単純な排他制御で十分な場合は、より軽量で安全な std::lock_guard を使い、ロック試行や手動アンロックといった、より柔軟な制御が必要な場合にのみ std::unique_lock を使うのがベストプラクティスです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次