DEV Community

MinBapE
MinBapE

Posted on

Mutex and Lock Guard in C++

Mutex (Mutual Exclusion)

Mutex is a synchronization object that controls access to shared resources in a multithreaded environment.
It is used to prevent Race Conditions that can occur when multiple threads access the same resource simultaneously.

Code Example

#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();
        counter++; // Critical Section
        mtx.unlock();
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

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

    std::cout << "Counter: " << counter << std::endl;
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Key Methods

  • lock()
    • Locks the mutex in a blocking manner.
    • If another thread has already acquired the lock, it waits until it is released.
  • unlock()
    • Releases the acquired lock.
    • Calling this from a thread that hasn't acquired the lock results in undefined behavior.
  • try_lock()
    • Attempts to lock in a non-blocking manner, returning true on success and false immediately on failure.
    • Allows performing other tasks without waiting to acquire the lock.

As shown in the example code above, the code region between lock() and unlock() (Critical Section) can only be executed by one thread at a time.
However, if an exception occurs or an early return happens before calling unlock(), a deadlock will occur.

Mutex for Special Situations

  • recursive_mutex
    • A mutex that allows the same thread to acquire the lock multiple times.
    • Must call unlock() as many times as the lock was acquired to fully release it.
  • timed_mutex
    • A mutex that allows specifying a timeout.
    • Provides try_lock_for() and try_lock_until() methods.
std::timed_mutex tmtx;

void function() {
    if (tmtx.try_lock_for(std::chrono::seconds(1))) {
        // Successfully acquired lock within 1 second
        // Critical Section
        tmtx.unlock();
    } else {
        // Timeout occurred
    }
}
Enter fullscreen mode Exit fullscreen mode
  • shared_mutex
    • Implements a Reader-Writer Lock.
    • Supported from C++17.
    • Multiple threads can perform read operations simultaneously, but write operations are performed exclusively.

Lock Guard

Manually managing lock() and unlock() is dangerous. The solution to this problem is the RAII (Resource Acquisition Is Initialization) pattern. It acquires resources in the constructor and releases them in the destructor, utilizing C++'s stack unwinding mechanism to ensure resources are safely released even when exceptions occur.

lock guard is one of the classes provided by the C++ standard library that helps reduce mistakes in mutex management.
When using lock guard, the mutex is automatically locked, and when the scope is exited, the lock_guard's destructor is called to automatically release the mutex.

lock_guard

  • The most basic RAII-based mutex wrapper.
  • Automatically calls lock() on construction and unlock() on destruction.
  • The simplest with minimal overhead.
  • Acquires the lock immediately upon creation.
  • Cannot control when the lock is released.
  • Cannot be copied or moved.
#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // Lock on creation
        counter++;
        // Automatically unlocks when leaving scope
    }
}

void safe_function() {
    std::lock_guard<std::mutex> lock(mtx);
    if (some_condition)
        return;
    process_data();  // Unlock guaranteed even if exception occurs
}
Enter fullscreen mode Exit fullscreen mode

unique_lock

  • Provides various features including deferred locking, condition variable integration, and ownership transfer.
  • Can manually call lock()/unlock().
  • Essential for use with condition variables like condition_variable.
  • Movable but not copyable.
std::mutex mtx;

void flexible_function() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);  // Deferred locking
    prepare_data();
    lock.lock();  // Manually lock at the needed point
    modify_shared_data();
    lock.unlock();  // Can manually release
    cleanup(); // Other tasks without lock

    // Automatically unlocks if still locked when scope ends
}
Enter fullscreen mode Exit fullscreen mode
// Use with condition variables (most common case)
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void wait_for_signal() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });  // unique_lock required
    process_data();
}
Enter fullscreen mode Exit fullscreen mode

scoped_lock

  • Supported from C++17.
  • Used to prevent deadlocks when locking multiple mutexes simultaneously.
  • Uses the std::lock() algorithm internally.
  • Identical to lock_guard for a single mutex.
std::mutex mtx1, mtx2;

// Code that can cause deadlock
void thread1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::lock_guard<std::mutex> lock2(mtx2);  // Order issue
    // ...
}

void thread2() {
    std::lock_guard<std::mutex> lock2(mtx2);
    std::lock_guard<std::mutex> lock1(mtx1);  // Opposite order!
    // ...
}

// Prevent deadlock with scoped_lock
void safe_thread1() {
    std::scoped_lock lock(mtx1, mtx2);  // Uses deadlock avoidance algorithm
    // ...
}

void safe_thread2() {
    std::scoped_lock lock(mtx2, mtx1);  // Safe regardless of order
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Lock Guard is a method for safely managing mutexes using the RAII pattern.
Since locks are automatically released even in exception or early return situations, it is much safer than manually managing lock()/unlock().

In most cases, lock_guard is sufficient, and unique_lock or scoped_lock should only be used in special situations.
Proper use of these can prevent many bugs that can occur in multithreaded programming.

Top comments (0)