第 18.19.2 節

mutex 与 lock_guard

0瀏覽次數0訪問次數--跳出率--平均停留

What problem does this section solve?

When multiple threads modify the same variable simultaneously, ++counter is not an atomic operation. It typically involves reading, incrementing, and writing back the value. When two threads execute these steps in an interleaved manner, a data race occurs.

The most common approach to protect shared data is to use the std::mutex mutex; after locking, only one thread can enter the critical section. std::lock_guard is an RAII lock that locks upon construction and automatically unlocks when going out of scope.

Example code

Example 1: Protecting a shared counter with mutex and lock_guard

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

int counter = 0;
// 加锁用来保护共享数据,避免多个线程同时修改造成数据竞争。
std::mutex counter_mutex;

void add_many(int times)
{
    for (int i = 0; i < times; ++i)
    {
        std::lock_guard<std::mutex> lock(counter_mutex);
        ++counter;
    }
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    // 创建子线程,让这部分代码和 main 线程并发运行。
    std::thread t1(add_many, 100000);
    std::thread t2(add_many, 100000);

    // join 会等待子线程结束,避免 main 提前退出。
    t1.join();
    t2.join();

    std::cout << "counter = " << counter << "\n";
    std::cout << "expected = 200000\n";

    return 0;
}

Results

counter = 200000
expected = 200000

Example 2: The scope of a lock should be minimized.

In this example, the thread prepares data outside of the lock and only acquires it when updating the shared total. This way, another thread doesn't have to wait on unrelated work.

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

int total = 0;
// 加锁用来保护共享数据,避免多个线程同时修改造成数据竞争。
std::mutex total_mutex;

int sum_part(const std::vector<int>& data, int begin, int end)
{
    int local_sum = 0;
    for (int i = begin; i < end; ++i)
    {
        local_sum += data[i];
    }
    return local_sum;
}

void worker(const std::vector<int>& data, int begin, int end)
{
    int local_sum = sum_part(data, begin, end);

    {
        std::lock_guard<std::mutex> lock(total_mutex);
        total += local_sum;
    }
}

int main()
{
    // 程序从 main 函数开始执行,下面的语句会按顺序运行。
    std::vector<int> data = {1, 2, 3, 4, 5, 6};

    // 创建子线程,让这部分代码和 main 线程并发运行。
    std::thread t1(worker, std::cref(data), 0, 3);
    std::thread t2(worker, std::cref(data), 3, 6);

    // join 会等待子线程结束,避免 main 提前退出。
    t1.join();
    t2.join();

    std::cout << "total = " << total << "\n";

    return 0;
}

Results

total = 21

The distinction between lock_guard and unique_lock

lock_guard and unique_lock are both RAII (Resource Acquisition Is Initialization) wrappers for mutexes in C++, but they differ significantly in functionality:

std::lock_guard

  • Provides the simplest form of mutex ownership.
  • Acquires the mutex on construction and releases it on destruction.
  • Cannot be unlocked manually before the guard goes out of scope.
  • Cannot be locked or unlocked again after initial acquisition.
  • Has virtually no overhead, making it very efficient.
  • Ideal for straightforward, scope-based locking patterns.

std::unique_lock

  • A more flexible and feature-rich locking mechanism.
  • Supports manual lock and unlock operations at any time.
  • Can be "deferred" or "try-locked" during construction.
  • Can be transferred between threads (moveable but not copyable).
  • Supports features like std::condition_variable (which requires unique_lock).
  • Involves slightly more overhead due to its additional bookkeeping.
  • Useful when you need to manipulate the lock's state or use it with condition variables.

Summary Use lock_guard for basic, deterministic locking. Use unique_lock when you need more control over the locking mechanism, such as locking/unlocking manually or working with condition variables.

lock typeFeaturesCommon uses
std::lock_guardSimple, construction locks, destruction unlocks, manual unlocking is not permitted.Protect a small segment of shared data access.
std::unique_lockDelayed locking, manual unlocking, ownership transferConditions and scenarios where locks need to be released early

When starting out, prioritize using lock_guard. Only use unique_lock when you need to work with condition_variable or require more flexible control over locking.

Common Errors

  1. Multiple threads are concurrently modifying shared data without using mutex or atomic for protection.
  2. After manually handling lock(), if you forget to unlock(), or encounter an error midway that prevents unlocking.
  3. Avoid performing time-consuming operations while holding a lock, such as sleeping, making network requests, or performing file I/O. These actions can lead to unnecessary delays and reduced system performance, as other threads or processes may be blocked from acquiring the lock and proceeding. Instead, release the lock before executing such operations, or use asynchronous alternatives to ensure efficient resource utilization.
  4. The scope of lock protection is too narrow, missing locks on some paths that read or write shared data.

Summary

  • Shared data requires synchronization protection.
  • std::mutex provides mutex access.
  • std::lock_guard is an RAII lock, locking on construction and unlocking on destruction.
  • The smaller the lock scope, the better, but it must fully cover all shared data access.
音乐页