[C++] How to Use std::lock_guard and std::unique_lock | Safe Mutex Management

目次

Introduction

In multi-threaded programming, if multiple threads access the same data (shared resource) simultaneously, it can lead to data corruption or unexpected race conditions. To prevent this, we use exclusive control (locking) with std::mutex.

However, if you manually manage mutex.lock() and mutex.unlock(), there is a risk of deadlocks if a function throws an exception or returns early, causing unlock() to be skipped.

To solve this problem, C++ provides two classes based on the RAII (Resource Acquisition Is Initialization) principle: std::lock_guard and std::unique_lock. These classes acquire the lock when the object is created and automatically release it when the object goes out of scope.

1. std::lock_guard: Simple and Reliable Lock Management

std::lock_guard is the simplest lock management class with the least overhead. It provides basic scope-based exclusive locking functionality.

Features

  • Locks the mutex in the constructor.
  • Automatically unlocks the mutex in the destructor (when exiting the scope).
  • Cannot manually unlock or transfer ownership of the lock in the middle of the scope.

Sample Code

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

// Mutex to protect shared resource (standard output in this example)
std::mutex mtx;

void print_message(const std::string& message) {
    // mtx is locked as soon as the lock_guard object is created
    std::lock_guard<std::mutex> guard(mtx);
    
    // Only one thread can execute inside this block at a time
    std::cout << message << " : Start" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate some heavy processing
    std::cout << message << " : End" << std::endl;
    
    // When exiting this function's scope, guard's destructor is called 
    // and mtx is automatically unlocked
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(print_message, "Thread " + std::to_string(i));
    }

    for (auto& th : threads) {
        th.join();
    }
    
    return 0;
}

Explanation: In the print_message function, lock_guard ensures that unlock is called when exiting the scope, so you do not have to worry about forgetting to call lock/unlock.

2. std::unique_lock: More Flexible Lock Management

std::unique_lock provides a more advanced and flexible lock management mechanism in addition to the features of lock_guard.

Features

  • Scope-based automatic lock/unlock, similar to lock_guard.
  • Allows manual locking/unlocking (.lock(), .unlock()).
  • Allows attempting to lock (.try_lock()).
  • Can transfer (move) ownership of the lock.

Sample Code

#include <iostream>
#include <thread>
#include <mutex>

std::mutex resource_mutex;

void process_data(int id) {
    // 1. Create unique_lock object without locking (defer_lock)
    std::unique_lock<std::mutex> lock(resource_mutex, std::defer_lock);

    std::cout << "Thread " << id << " is attempting to lock..." << std::endl;

    // 2. Try to lock
    if (lock.try_lock()) {
        std::cout << "Thread " << id << " successfully locked." << std::endl;
        // ... Processing using shared resource ...
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
        
        // 3. Manually unlock (if you want to release early)
        lock.unlock();
        std::cout << "Thread " << id << " released the lock." << std::endl;
    } else {
        std::cout << "Thread " << id << " failed to lock." << std::endl;
    }
}

int main() {
    std::thread t1(process_data, 1);
    std::thread t2(process_data, 2);

    t1.join();
    t2.join();

    return 0;
}

Explanation: unique_lock is a powerful tool when implementing complex locking strategies, such as using try_lock (returns true if locked, false if not).

Summary

Featurestd::lock_guardstd::unique_lock
Basic FunctionScope-based automatic lock/unlockScope-based automatic lock/unlock
Simplicity◎ (Very Simple)
Overhead◎ (Almost Zero)△ (Slightly larger)
Manual Lock/UnlockNot PossiblePossible
Try Lock (try_lock)Not PossiblePossible
Ownership TransferNot PossiblePossible

In conclusion, the best practice is to use the lighter and safer std::lock_guard when simple exclusive control is sufficient, and use std::unique_lock only when flexible control such as lock attempts or manual unlocking is required.

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

この記事を書いた人

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

目次