C++11—mutex

mutex

1. 概述

在多线程编程中,当多个线程同时访问共享资源时,如果没有适当的同步机制,就会导致数据竞争 (Data Race)和未定义行为std::mutex(互斥锁)是 C++11 标准库提供的最基础的线程同步原语,用于保护共享资源,确保同一时刻只有一个线程能够访问被保护的代码区域。

1.1 为什么需要互斥锁?

想象一下银行取款的场景:如果多个客户同时从同一个账户取款,而没有适当的保护机制,账户余额可能会被错误地计算。互斥锁就像银行柜台的"正在服务"指示灯,当一个客户在办理业务时,其他客户必须等待。

在多线程程序中,互斥锁的作用是:

  • 防止数据竞争:确保对共享数据的访问是互斥的
  • 保证数据一致性:避免多个线程同时修改同一数据导致的不一致状态
  • 实现线程同步:协调多个线程的执行顺序

1.2 头文件

使用 std::mutex 需要包含头文件:

cpp 复制代码
#include <mutex>

2. std::mutex 基础

2.1 类定义

cpp 复制代码
class mutex
{
public:
    mutex() noexcept;
    ~mutex();
    
    mutex(const mutex&) = delete;           // 禁止拷贝
    mutex& operator=(const mutex&) = delete; // 禁止赋值
    
    void lock();        // 加锁(阻塞)
    bool try_lock();    // 尝试加锁(非阻塞)
    void unlock();      // 解锁
    // native_handle_type native_handle();  // 获取底层句柄(可选,平台相关)
};

2.2 基本使用

2.2.1 手动加锁和解锁
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
int counter = 0;

void IncrementCounter(int times)
{
    for (int i = 0; i < times; ++i)
    {
        mtx.lock();        // 加锁
        ++counter;         // 临界区代码
        mtx.unlock();      // 解锁
    }
}

int main()
{
    std::vector<std::thread> threads;
    
    // 创建 5 个线程,每个线程增加 counter 1000 次
    for (int i = 0; i < 5; ++i)
    {
        threads.emplace_back(IncrementCounter, 1000);
    }
    
    // 等待所有线程完成
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "最终 counter 值: " << counter << std::endl;
    // 输出: 最终 counter 值: 5000
    
    return 0;
}

注意事项

  1. 必须配对使用 :每次 lock() 必须对应一次 unlock(),否则会导致死锁

    为什么会死锁?

    死锁(Deadlock)是指线程因为无法获取锁而永远阻塞的情况。如果忘记调用 unlock()

    • 同一线程再次加锁 :如果同一个线程在未解锁的情况下再次调用 lock(),会导致死锁(对于非递归互斥锁)
    • 其他线程无法获取锁:锁永远不会被释放,其他需要该锁的线程会永远等待

    示例:

    cpp 复制代码
    #include <iostream>
    #include <thread>
    #include <mutex>
    
    std::mutex mtx;
    int counter = 0;
    
    // ❌ 错误示例:忘记解锁导致死锁
    void BadFunction()
    {
        mtx.lock();
        ++counter;
        // 忘记调用 mtx.unlock()
        // 函数返回后,锁永远不会被释放
    }
    
    void AnotherFunction()
    {
        mtx.lock();  // 这里会永远等待,因为 BadFunction() 没有释放锁
        ++counter;
        mtx.unlock();
    }
    
    int main()
    {
        std::thread t1(BadFunction);
        std::thread t2(AnotherFunction);
        
        t1.join();
        t2.join();  // 程序会在这里永远阻塞(死锁)
        
        return 0;
    }

    在这个例子中:

    • t1 线程调用 BadFunction(),获取锁后忘记释放
    • t2 线程调用 AnotherFunction(),尝试获取同一个锁
    • 由于锁从未被释放,t2 会永远等待,程序陷入死锁
  2. 异常安全:如果在临界区代码中抛出异常,可能导致锁无法释放

    什么是临界区?

    **临界区(Critical Section)**是指被互斥锁保护的代码段,即从 lock()unlock() 之间的代码。在上面的示例中:

    cpp 复制代码
    mtx.lock();        // ← 临界区开始
    ++counter;         // ← 临界区代码(被保护的部分)
    mtx.unlock();      // ← 临界区结束

    临界区的特点:

    • 互斥性:同一时刻只有一个线程能进入临界区,其他线程必须等待
    • 串行化:多个线程对临界区的访问是串行的,有明确的执行顺序
    • 可见性:线程在临界区内的修改,在释放锁后对其他线程可见

    如果在临界区代码中抛出异常,且没有使用 RAII,锁可能无法释放:

    cpp 复制代码
    // ❌ 错误示例:异常导致锁无法释放
    void UnsafeFunction()
    {
        mtx.lock();           // 加锁
        SomeFunction();        // 如果这里抛出异常,下面的 unlock() 不会执行
        mtx.unlock();          // 锁永远不会被释放,导致死锁
    }
  3. 不推荐直接使用 :建议使用 RAII 包装器(如 std::lock_guard)来管理锁

2.3 异常安全问题

直接使用 lock()unlock() 存在异常安全风险:

cpp 复制代码
// 示例函数(实际使用时替换为真实函数)
void SomeFunction()
{
    // 可能抛出异常的操作
}

// ❌ 错误示例:异常不安全
void UnsafeFunction()
{
    mtx.lock();
    SomeFunction();  // 如果这里抛出异常,锁永远不会被释放
    mtx.unlock();
}

// ✅ 正确示例:使用 RAII
void SafeFunction()
{
    std::lock_guard<std::mutex> lock(mtx);  // 构造时自动加锁
    SomeFunction();  // 即使抛出异常,析构时也会自动解锁
    // lock 对象析构时自动调用 unlock()
}

3. RAII 锁管理

C++11 提供了两种 RAII(Resource Acquisition Is Initialization)锁管理类,用于自动管理锁的生命周期,确保异常安全。

3.1 std::lock_guard

std::lock_guard 是最简单的锁管理类,构造时加锁,析构时解锁。

3.1.1 类定义
cpp 复制代码
template<class Mutex>
class lock_guard
{
public:
    explicit lock_guard(Mutex& m);           // 构造时加锁
    lock_guard(Mutex& m, std::adopt_lock_t); // 假设已经持有锁
    ~lock_guard();                           // 析构时解锁
    
    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
};
3.1.2 基本使用
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
int counter = 0;

void IncrementCounter(int times)
{
    for (int i = 0; i < times; ++i)
    {
        std::lock_guard<std::mutex> lock(mtx);  // 自动加锁
        ++counter;                               // 临界区代码
        // lock 对象析构时自动解锁
    }
}

int main()
{
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 5; ++i)
    {
        threads.emplace_back(IncrementCounter, 1000);
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "最终 counter 值: " << counter << std::endl;
    
    return 0;
}
3.1.3 adopt_lock 用法

adopt_lock 用于已经持有锁 的情况,它告诉 lock_guard 不要再次加锁,而是假设锁已经被当前线程持有,只需要在析构时解锁即可。

为什么需要 adopt_lock?

在某些场景下,我们需要先手动加锁(比如使用 std::lock 同时锁定多个互斥锁),然后希望使用 RAII 机制自动管理锁的释放。adopt_lock 就是为这种场景设计的。

工作原理
  • 不使用 adopt_locklock_guard 构造时会调用 lock(),如果锁已经被持有,会导致死锁
  • 使用 adopt_locklock_guard 构造时不会 调用 lock(),只是记录锁的所有权,析构时调用 unlock()
使用场景

场景 1:配合 std::lock 使用

这是 adopt_lock 最常见的用法,用于在同时锁定多个互斥锁后,使用 RAII 管理锁的释放:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1;
std::mutex mtx2;

void FunctionWithAdoptLock()
{
    // 使用 std::lock 同时锁定多个互斥锁(避免死锁)
    std::lock(mtx1, mtx2);
    
    // 使用 adopt_lock 告诉 lock_guard 我们已经持有锁
    // lock_guard 不会再次加锁,只负责在析构时解锁
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    
    // 临界区代码
    // lock1 和 lock2 析构时会自动解锁
}

场景 2:条件加锁

在某些条件下才需要加锁,但希望统一使用 RAII 管理:

cpp 复制代码
void ConditionalLockFunction(bool needLock)
{
    if (needLock)
    {
        mtx.lock();  // 手动加锁
        
        // 使用 adopt_lock 让 lock_guard 接管锁的管理
        std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
        
        // 临界区代码
        // lock 析构时自动解锁
    }
    else
    {
        // 不需要锁的操作
    }
}
注意事项
  1. 必须先加锁 :使用 adopt_lock 之前,必须确保锁已经被当前线程持有,否则会导致未定义行为
  2. 不能重复加锁 :如果锁已经被持有,使用 adopt_lock 不会再次加锁,这是正确的行为
  3. 自动解锁lock_guard 析构时会自动调用 unlock(),无需手动解锁
错误示例
cpp 复制代码
// ❌ 错误:没有先加锁就使用 adopt_lock
void BadFunction()
{
    // 没有调用 mtx.lock()
    std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
    // 未定义行为:锁没有被持有,但 lock_guard 认为已经持有
}

// ❌ 错误:使用 adopt_lock 后又手动解锁
void BadFunction2()
{
    mtx.lock();
    std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
    // 临界区代码
    mtx.unlock();  // 错误:lock_guard 析构时还会再次解锁,导致未定义行为
}

// ✅ 正确:先加锁,再使用 adopt_lock,让 lock_guard 自动管理
void GoodFunction()
{
    mtx.lock();
    std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
    // 临界区代码
    // lock 析构时自动解锁,无需手动调用 unlock()
}
完整示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx1;
std::mutex mtx2;
int data1 = 0;
int data2 = 0;

void SafeUpdate()
{
    // 使用 std::lock 同时锁定两个互斥锁(避免死锁)
    std::lock(mtx1, mtx2);
    
    // 使用 adopt_lock 让 lock_guard 接管锁的管理
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    
    // 现在可以安全地修改两个共享变量
    ++data1;
    ++data2;
    
    // lock1 和 lock2 析构时会自动解锁
}

int main()
{
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 5; ++i)
    {
        threads.emplace_back(SafeUpdate);
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "data1: " << data1 << ", data2: " << data2 << std::endl;
    
    return 0;
}

3.2 std::unique_lock

std::unique_lockstd::lock_guard 更灵活,支持延迟加锁、条件变量等高级功能。

3.2.1 类定义
cpp 复制代码
template<class Mutex>
class unique_lock
{
public:
    unique_lock() noexcept;                    // 默认构造(不持有锁)
    explicit unique_lock(Mutex& m);            // 构造时加锁
    unique_lock(Mutex& m, std::defer_lock_t);  // 延迟加锁
    unique_lock(Mutex& m, std::try_to_lock_t); // 尝试加锁
    unique_lock(Mutex& m, std::adopt_lock_t);  // 假设已经持有锁
    ~unique_lock();                            // 析构时解锁
    
    void lock();           // 加锁
    bool try_lock();       // 尝试加锁
    void unlock();         // 解锁
    bool owns_lock() const; // 检查是否持有锁
    Mutex* release();      // 释放锁的所有权(不解锁)
};
3.2.2 基本使用
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
int counter = 0;

void IncrementCounter(int times)
{
    for (int i = 0; i < times; ++i)
    {
        std::unique_lock<std::mutex> lock(mtx);  // 自动加锁
        ++counter;
        // lock 析构时自动解锁
    }
}

int main()
{
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 5; ++i)
    {
        threads.emplace_back(IncrementCounter, 1000);
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "最终 counter 值: " << counter << std::endl;
    
    return 0;
}
3.2.3 延迟加锁(defer_lock)

defer_lock 用于延迟加锁,构造 unique_lock不立即加锁,而是稍后根据需要手动加锁。这提供了更灵活的锁控制。

工作原理
  • 不使用 defer_lockunique_lock 构造时立即调用 lock() 加锁
  • 使用 defer_lockunique_lock 构造时不调用 lock(),锁对象处于"未锁定"状态,可以稍后调用 lock() 加锁
使用场景

场景 1:需要先执行非临界区代码,再进入临界区

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void FunctionWithDeferLock()
{
    // 创建 unique_lock,但不立即加锁
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    
    // 做一些不需要锁的准备工作(减少锁的持有时间)
    int localData = 100;
    // 进行一些计算...
    
    // 现在才加锁,进入临界区
    lock.lock();
    counter += localData;  // 临界区代码
    lock.unlock();  // 可以手动解锁,释放锁
    
    // 做一些不需要锁的后处理工作
    // 清理工作...
    
    // 如果需要,可以再次加锁
    lock.lock();
    // 更多临界区代码
    // lock 析构时自动解锁
}

场景 2:配合 std::lock 同时锁定多个互斥锁

这是 defer_lock 最常见的用法,用于避免死锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1;
std::mutex mtx2;

void SafeFunction()
{
    // 创建两个 unique_lock,但不立即加锁
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
    
    // 使用 std::lock 同时锁定两个锁(避免死锁)
    std::lock(lock1, lock2);
    
    // 临界区代码
    // lock1 和 lock2 析构时自动解锁
}

场景 3:条件加锁

根据条件决定是否需要加锁:

cpp 复制代码
void ConditionalLockFunction(bool needLock)
{
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    
    if (needLock)
    {
        lock.lock();  // 只在需要时才加锁
        // 临界区代码
    }
    else
    {
        // 不需要锁的操作
    }
    // 如果持有锁,析构时自动解锁
}
注意事项
  1. 必须手动加锁 :使用 defer_lock 后,必须手动调用 lock() 才能进入临界区
  2. 可以多次加锁解锁unique_lock 支持多次调用 lock()unlock()
  3. 检查锁状态 :可以使用 owns_lock() 检查当前是否持有锁
  4. 异常安全 :即使忘记手动加锁,unique_lock 析构时也不会出错(因为没有持有锁)
错误示例
cpp 复制代码
// ❌ 错误:使用 defer_lock 后忘记加锁
void BadFunction()
{
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    // 忘记调用 lock.lock()
    // 直接访问共享资源,没有锁保护,导致数据竞争
    counter++;
}

// ✅ 正确:使用 defer_lock 后记得加锁
void GoodFunction()
{
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    lock.lock();  // 必须手动加锁
    counter++;
    // lock 析构时自动解锁
}
完整示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx1;
std::mutex mtx2;
int data1 = 0;
int data2 = 0;

void SafeUpdate()
{
    // 使用 defer_lock 创建锁对象,但不立即加锁
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
    
    // 做一些不需要锁的准备工作
    int localValue1 = 10;
    int localValue2 = 20;
    
    // 使用 std::lock 同时锁定两个锁(避免死锁)
    std::lock(lock1, lock2);
    
    // 现在可以安全地修改共享数据
    data1 += localValue1;
    data2 += localValue2;
    
    // lock1 和 lock2 析构时自动解锁
}

int main()
{
    std::vector<std::thread> threads;
    
    for (int i = 0; i < 5; ++i)
    {
        threads.emplace_back(SafeUpdate);
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "data1: " << data1 << ", data2: " << data2 << std::endl;
    
    return 0;
}
3.2.4 尝试加锁(try_to_lock)

try_to_lock 用于非阻塞地尝试加锁,如果锁不可用,立即返回而不等待。这对于需要避免阻塞的场景非常有用。

工作原理
  • 不使用 try_to_lockunique_lock 构造时调用 lock(),如果锁不可用会阻塞等待
  • 使用 try_to_lockunique_lock 构造时调用 try_lock(),如果锁不可用立即返回,不阻塞
使用场景

场景 1:避免阻塞,执行替代操作

当无法获取锁时,执行其他操作而不是等待:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx;
int counter = 0;

void FunctionWithTryLock()
{
    // 尝试获取锁,不阻塞
    std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
    
    if (lock.owns_lock())  // 检查是否成功获取锁
    {
        // 成功获取锁,执行临界区代码
        ++counter;
        std::cout << "成功获取锁,更新 counter" << std::endl;
    }
    else
    {
        // 未能获取锁,执行其他操作
        std::cout << "无法获取锁,执行其他操作" << std::endl;
        // 可以执行一些不需要锁的操作
    }
    // lock 析构时,如果持有锁会自动解锁
}

场景 2:实现超时机制

结合循环和延迟,实现类似超时的效果:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::mutex mtx;

void TryLockWithTimeout()
{
    const int maxAttempts = 10;
    const auto delay = std::chrono::milliseconds(100);
    
    for (int i = 0; i < maxAttempts; ++i)
    {
        std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
        
        if (lock.owns_lock())
        {
            // 成功获取锁
            std::cout << "成功获取锁,执行操作" << std::endl;
            return;
        }
        
        // 未能获取锁,等待一段时间后重试
        std::this_thread::sleep_for(delay);
    }
    
    // 超时,执行替代操作
    std::cout << "超时,无法获取锁" << std::endl;
}

场景 3:避免死锁

在需要多个锁时,如果无法获取所有锁,立即放弃:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1;
std::mutex mtx2;

void TryLockMultiple()
{
    std::unique_lock<std::mutex> lock1(mtx1, std::try_to_lock);
    
    if (!lock1.owns_lock())
    {
        // 无法获取第一个锁,立即返回
        std::cout << "无法获取 mtx1,放弃操作" << std::endl;
        return;
    }
    
    std::unique_lock<std::mutex> lock2(mtx2, std::try_to_lock);
    
    if (!lock2.owns_lock())
    {
        // 无法获取第二个锁,释放第一个锁并返回
        std::cout << "无法获取 mtx2,放弃操作" << std::endl;
        return;
    }
    
    // 成功获取所有锁,执行操作
    std::cout << "成功获取所有锁,执行操作" << std::endl;
}

场景 4:优先级处理

高优先级任务可以尝试获取锁,如果失败则跳过:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void HighPriorityTask()
{
    std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
    
    if (lock.owns_lock())
    {
        // 成功获取锁,执行高优先级任务
        std::cout << "执行高优先级任务" << std::endl;
    }
    else
    {
        // 无法获取锁,跳过本次执行
        std::cout << "资源被占用,跳过本次执行" << std::endl;
    }
}
注意事项
  1. 必须检查锁状态 :使用 try_to_lock 后,必须使用 owns_lock() 检查是否成功获取锁
  2. 非阻塞try_to_lock 不会阻塞,立即返回
  3. 可能失败:锁可能被其他线程持有,尝试加锁可能失败
  4. 异常安全 :即使没有获取锁,unique_lock 析构时也不会出错
错误示例
cpp 复制代码
// ❌ 错误:没有检查锁状态就使用
void BadFunction()
{
    std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
    // 没有检查 owns_lock()
    counter++;  // 如果锁获取失败,这里没有锁保护,导致数据竞争
}

// ✅ 正确:检查锁状态后再使用
void GoodFunction()
{
    std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
    
    if (lock.owns_lock())  // 必须检查
    {
        counter++;  // 只有在持有锁时才访问共享资源
    }
}
完整示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>

std::mutex mtx;
int counter = 0;

void WorkerThread(int threadId)
{
    for (int i = 0; i < 10; ++i)
    {
        // 尝试获取锁
        std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
        
        if (lock.owns_lock())
        {
            // 成功获取锁,更新共享资源
            ++counter;
            std::cout << "线程 " << threadId << " 更新 counter: " << counter << std::endl;
            
            // 模拟一些工作
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
        }
        else
        {
            // 无法获取锁,执行其他操作
            std::cout << "线程 " << threadId << " 无法获取锁,跳过本次操作" << std::endl;
            
            // 等待一段时间后重试
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
}

int main()
{
    std::vector<std::thread> threads;
    
    // 创建 3 个线程
    for (int i = 0; i < 3; ++i)
    {
        threads.emplace_back(WorkerThread, i + 1);
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "最终 counter: " << counter << std::endl;
    
    return 0;
}
try_to_lock vs try_lock()

std::unique_locktry_to_lock 构造函数内部调用的是互斥锁的 try_lock() 方法:

cpp 复制代码
// 这两种方式是等价的
std::unique_lock<std::mutex> lock1(mtx, std::try_to_lock);

// 等价于
std::unique_lock<std::mutex> lock2(mtx, std::defer_lock);
if (lock2.try_lock())
{
    // 成功获取锁
}
3.2.5 lock_guard vs unique_lock
特性 lock_guard unique_lock
构造时加锁 可选
手动解锁
延迟加锁
尝试加锁
条件变量 不支持 支持
性能开销 稍高
灵活性
为什么有这些差异?

1. 构造时加锁:为什么 lock_guard 是"是",unique_lock 是"可选"?

原因lock_guard 设计为简单、轻量级的锁管理工具,只提供最基本的 RAII 功能。unique_lock 设计为更灵活的锁管理工具,支持多种加锁策略。

对比示例

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

// lock_guard:构造时必须加锁
void FunctionWithLockGuard()
{
    // ✅ 正确:构造时自动加锁
    std::lock_guard<std::mutex> lock(mtx);
    // 临界区代码
    
    // ❌ 错误:lock_guard 不支持延迟加锁
    // std::lock_guard<std::mutex> lock(mtx, std::defer_lock);  // 编译错误
}

// unique_lock:构造时可以延迟加锁
void FunctionWithUniqueLock()
{
    // ✅ 方式1:构造时自动加锁
    std::unique_lock<std::mutex> lock1(mtx);
    
    // ✅ 方式2:延迟加锁
    std::unique_lock<std::mutex> lock2(mtx, std::defer_lock);
    lock2.lock();  // 稍后手动加锁
}

2. 手动解锁:为什么 lock_guard 是"否",unique_lock 是"是"?

原因lock_guard 设计为"获取即锁定,析构即解锁"的简单模式,不支持手动控制。unique_lock 提供更细粒度的控制,允许手动解锁以减小锁的持有时间。

对比示例

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

// lock_guard:不支持手动解锁
void FunctionWithLockGuard()
{
    std::lock_guard<std::mutex> lock(mtx);
    
    // 临界区代码
    ++counter;
    
    // ❌ 错误:lock_guard 没有 unlock() 方法
    // lock.unlock();  // 编译错误
    
    // 只能等待 lock 析构时自动解锁
}

// unique_lock:支持手动解锁(无论是否使用 defer_lock)
void FunctionWithUniqueLock()
{
    // 方式1:不使用 defer_lock,构造时自动加锁
    std::unique_lock<std::mutex> lock(mtx);  // 构造时自动加锁
    
    // 临界区代码
    ++counter;
    
    // ✅ 正确:即使没有使用 defer_lock,也可以手动解锁
    // unique_lock 无论构造时是否加锁,都支持 unlock() 方法
    lock.unlock();  // 手动解锁,此时 lock 不再持有锁
    
    // 做一些不需要锁的操作(锁已释放,其他线程可以获取)
    // DoSomeWork();
    
    // 如果需要,可以再次加锁
    lock.lock();  // 再次加锁
    // 更多临界区代码
    // lock 析构时自动解锁(如果持有锁的话)
}

// 对比:使用 defer_lock 的情况
void FunctionWithDeferLock()
{
    // 方式2:使用 defer_lock,构造时不加锁
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);  // 构造时**不**加锁
    
    // 做一些不需要锁的操作
    // DoSomeWork();
    
    lock.lock();  // 手动加锁
    // 临界区代码
    ++counter;
    
    lock.unlock();  // 手动解锁(同样支持)
    
    // 如果需要,可以再次加锁
    lock.lock();
    // 更多临界区代码
    // lock 析构时自动解锁(如果持有锁的话)
}

// 总结:unique_lock 的 unlock() 方法不依赖于 defer_lock
// - 不使用 defer_lock:构造时自动加锁,可以手动解锁
// - 使用 defer_lock:构造时不加锁,需要手动加锁,也可以手动解锁
// 无论哪种方式,都支持手动解锁

3. 条件变量:为什么 lock_guard 不支持,unique_lock 支持?

原因 :条件变量(std::condition_variable)的 wait() 方法需要能够临时释放锁 ,等待条件满足后再重新获取锁。lock_guard 不支持手动解锁,无法满足这个需求。unique_lock 支持手动解锁和重新加锁,可以与条件变量配合使用。

对比示例

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> dataQueue;
bool ready = false;

// ❌ 错误:lock_guard 不能与条件变量配合使用
void BadFunction()
{
    std::lock_guard<std::mutex> lock(mtx);
    
    // ❌ 错误:condition_variable::wait() 需要能够释放和重新获取锁
    // lock_guard 不支持 unlock(),无法满足条件变量的需求
    // cv.wait(lock);  // 编译错误:lock_guard 没有 unlock() 方法
}

// ✅ 正确:unique_lock 可以与条件变量配合使用
void GoodFunction()
{
    std::unique_lock<std::mutex> lock(mtx);
    
    // 等待条件满足
    // wait() 会临时释放锁,等待条件满足后重新获取锁
    cv.wait(lock, []() { return ready; });
    
    // 条件满足后,继续执行(此时已重新持有锁)
    // 处理数据...
}

完整示例:生产者-消费者模式

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
bool finished = false;

// 生产者
void Producer()
{
    for (int i = 0; i < 10; ++i)
    {
        {
            std::lock_guard<std::mutex> lock(mtx);
            queue.push(i);
        }
        cv.notify_one();  // 通知消费者
    }
    
    {
        std::lock_guard<std::mutex> lock(mtx);
        finished = true;
    }
    cv.notify_one();  // 通知消费者结束
}

// 消费者
void Consumer()
{
    while (true)
    {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待队列非空或生产结束(wait 会临时释放锁,被唤醒后重新获取锁)
        cv.wait(lock, []() { return !queue.empty() || finished; });
        
        // 如果生产结束且队列为空,退出
        if (finished && queue.empty())
        {
            break;
        }
        
        // 处理一个数据
        if (!queue.empty())
        {
            int value = queue.front();
            queue.pop();
            lock.unlock();  // 解锁后再处理,减小锁的持有时间
            
            std::cout << "消费: " << value << std::endl;
            // 注意:已经手动解锁,lock 析构时不会再解锁(这是正确的行为)
        }
    }
}

int main()
{
    std::thread producer(Producer);
    std::thread consumer(Consumer);
    
    producer.join();
    consumer.join();
    
    return 0;
}

代码说明

  1. 同步机制

    • 互斥锁保护共享数据(队列和 finished 标志)
    • 条件变量实现线程间通知
    • wait() 在等待时释放锁,被唤醒后重新获取锁
  2. 生产者

    • 在锁保护下推入数据
    • 在锁外通知消费者(避免不必要的阻塞)
    • 设置结束标志后通知消费者
  3. 消费者

    • 使用 unique_lock 配合条件变量(lock_guard 不支持)
    • 等待队列非空或生产结束
    • 循环处理队列中的所有数据
    • 处理时解锁,减小锁的持有时间

总结

  • lock_guard:简单、高效,适合大多数简单场景
  • unique_lock:灵活、功能强大,适合需要精细控制或与条件变量配合的场景

选择建议

  • 简单场景 :使用 std::lock_guard
  • 需要条件变量 :使用 std::unique_lock
  • 需要手动控制锁 :使用 std::unique_lock
  • 性能敏感场景 :优先使用 std::lock_guard

4. 多锁管理

4.1 std::lock

std::lock 是一个函数模板,可以同时锁定多个互斥锁,避免死锁。

4.1.1 函数签名
cpp 复制代码
template<class Lockable1, class Lockable2, class... LockableN>
void lock(Lockable1& l1, Lockable2& l2, LockableN&... ln);
4.1.2 避免死锁

当需要同时持有多个锁时,如果加锁顺序不一致,可能导致死锁:

cpp 复制代码
// ❌ 错误示例:可能导致死锁
void Thread1()
{
    mtx1.lock();
    mtx2.lock();
    // 临界区代码
    mtx2.unlock();
    mtx1.unlock();
}

void Thread2()
{
    mtx2.lock();  // 与 Thread1 的加锁顺序相反
    mtx1.lock();  // 可能导致死锁
    // 临界区代码
    mtx1.unlock();
    mtx2.unlock();
}

使用 std::lock 可以避免死锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx1;
std::mutex mtx2;
int account1 = 1000;
int account2 = 1000;

// 转账函数:需要同时锁定两个账户
void Transfer(int amount)
{
    // std::lock 使用死锁避免算法,同时锁定多个互斥锁
    std::lock(mtx1, mtx2);
    
    // 使用 adopt_lock 告诉 lock_guard 我们已经持有锁
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    
    // 临界区:同时修改两个账户
    account1 -= amount;
    account2 += amount;
}

// 反向转账:加锁顺序不同
void ReverseTransfer(int amount)
{
    // 即使参数顺序与 Transfer 不同,std::lock 也能避免死锁
    std::lock(mtx2, mtx1);  // 顺序相反,但不会死锁
    
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    
    // 临界区:反向转账
    account2 -= amount;
    account1 += amount;
}

int main()
{
    std::vector<std::thread> threads;
    
    // 创建多个线程,同时进行转账和反向转账
    for (int i = 0; i < 5; ++i)
    {
        threads.emplace_back(Transfer, 100);
        threads.emplace_back(ReverseTransfer, 50);
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "最终余额:账户1 = " << account1 
              << ", 账户2 = " << account2 << std::endl;
    
    return 0;
}

这个案例说明了什么?

这个案例主要说明:即使不同函数中加锁顺序不同,std::lock 也能避免死锁

核心要点

  1. 问题场景Transfer 使用 std::lock(mtx1, mtx2)ReverseTransfer 使用 std::lock(mtx2, mtx1),加锁顺序相反

  2. 如果手动加锁会怎样?

    cpp 复制代码
    // ❌ 手动加锁:可能导致死锁
    void Transfer(int amount)
    {
        mtx1.lock();  // 线程1先锁 mtx1
        mtx2.lock();  // 线程1等待 mtx2
        // ...
    }
    
    void ReverseTransfer(int amount)
    {
        mtx2.lock();  // 线程2先锁 mtx2
        mtx1.lock();  // 线程2等待 mtx1(死锁!)
        // ...
    }
  3. std::lock 如何避免死锁?

    • std::lock 使用死锁避免算法,内部会尝试获取所有锁
    • 如果无法同时获取所有锁,会释放已获取的锁并重试
    • 这样确保了要么同时获取所有锁,要么一个都不获取
  4. 实际意义 :使用 std::lock 后,开发者不需要关心加锁顺序,可以安全地同时获取多个锁

std::lock 的工作原理

std::lock 使用死锁避免算法,大致流程如下:

  1. 尝试按顺序锁定所有互斥锁
  2. 如果某个锁无法获取,释放所有已获取的锁
  3. 等待一小段时间后重试
  4. 重复直到成功获取所有锁

这样确保了要么同时获取所有锁,要么一个都不获取,避免了死锁。

4.1.3 使用 unique_lock 的简化写法
cpp 复制代码
void SimplifiedSafeFunction()
{
    std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
    
    // 同时锁定两个锁
    std::lock(lock1, lock2);
    
    // 临界区代码
    // lock1 和 lock2 析构时自动解锁
}

4.2 std::try_lock

std::try_lock 尝试同时锁定多个互斥锁,如果任何一个锁无法获取,则释放所有已获取的锁并返回。

4.2.1 函数签名
cpp 复制代码
template<class Lockable1, class Lockable2, class... LockableN>
int try_lock(Lockable1& l1, Lockable2& l2, LockableN&... ln);

返回值:

  • 成功:返回 -1
  • 失败:返回第一个无法获取的锁的索引(从 0 开始)
4.2.2 使用示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1;
std::mutex mtx2;

void TryLockFunction()
{
    int result = std::try_lock(mtx1, mtx2);
    
    if (result == -1)
    {
        // 成功获取所有锁
        std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
        std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
        
        // 临界区代码
        std::cout << "成功获取所有锁" << std::endl;
    }
    else
    {
        // 未能获取所有锁
        std::cout << "无法获取锁,索引: " << result << std::endl;
    }
}

5. 其他类型的互斥锁

5.1 std::recursive_mutex

std::recursive_mutex 是递归互斥锁,允许同一线程多次加锁。

5.1.1 使用场景

当函数可能被递归调用,或者需要多次加锁时,使用递归互斥锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::recursive_mutex rmtx;

void RecursiveFunction(int depth)
{
    if (depth <= 0)
    {
        return;
    }
    
    std::lock_guard<std::recursive_mutex> lock(rmtx);  // 同一线程可以多次加锁
    
    std::cout << "深度: " << depth << std::endl;
    
    RecursiveFunction(depth - 1);  // 递归调用,再次尝试加锁(成功)
    
    // lock 析构时解锁
}

int main()
{
    std::thread t(RecursiveFunction, 5);
    t.join();
    
    return 0;
}

注意 :如果使用普通的 std::mutex,递归调用会导致死锁。

为什么会死锁?

普通 std::mutex非递归互斥锁,同一个线程不能多次加锁。如果线程已经持有锁,再次尝试加锁会导致死锁。

5.2 std::timed_mutex

std::timed_mutex 是带超时的互斥锁,支持尝试加锁和超时加锁。

5.2.1 类定义
cpp 复制代码
class timed_mutex
{
public:
    void lock();
    bool try_lock();
    template<class Rep, class Period>
    bool try_lock_for(const std::chrono::duration<Rep, Period>& timeout);
    template<class Clock, class Duration>
    bool try_lock_until(const std::chrono::time_point<Clock, Duration>& timeout);
    void unlock();
};
5.2.2 使用示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

std::timed_mutex tmtx;

void TimedLockFunction()
{
    // 尝试在 1 秒内获取锁
    if (tmtx.try_lock_for(std::chrono::seconds(1)))
    {
        std::lock_guard<std::timed_mutex> lock(tmtx, std::adopt_lock);
        
        std::cout << "成功获取锁" << std::endl;
        
        // 模拟一些工作
        std::this_thread::sleep_for(std::chrono::milliseconds(1500));
    }
    else
    {
        std::cout << "超时,未能获取锁" << std::endl;
    }
}

int main()
{
    std::thread t1(TimedLockFunction);
    std::thread t2(TimedLockFunction);
    
    t1.join();
    t2.join();
    
    return 0;
}

5.3 std::recursive_timed_mutex

std::recursive_timed_mutex 结合了递归互斥锁和超时互斥锁的特性。

6. 实际应用示例

6.1 线程安全的计数器

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

class ThreadSafeCounter
{
public:
    void Increment()
    {
        std::lock_guard<std::mutex> lock(mtx);
        ++value;
    }
    
    void Decrement()
    {
        std::lock_guard<std::mutex> lock(mtx);
        --value;
    }
    
    int GetValue() const
    {
        std::lock_guard<std::mutex> lock(mtx);
        return value;
    }

private:
    mutable std::mutex mtx;  // mutable 允许在 const 函数中加锁
    int value = 0;
};

int main()
{
    ThreadSafeCounter counter;
    std::vector<std::thread> threads;
    
    // 创建多个线程同时操作计数器
    for (int i = 0; i < 10; ++i)
    {
        if (i % 2 == 0)
        {
            threads.emplace_back([&counter]()
            {
                for (int j = 0; j < 1000; ++j)
                {
                    counter.Increment();
                }
            });
        }
        else
        {
            threads.emplace_back([&counter]()
            {
                for (int j = 0; j < 1000; ++j)
                {
                    counter.Decrement();
                }
            });
        }
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    std::cout << "最终值: " << counter.GetValue() << std::endl;
    
    return 0;
}

6.2 线程安全的队列(简化版)

cpp 复制代码
#include <queue>
#include <mutex>
#include <thread>
#include <chrono>
#include <iostream>

template<typename T>
class ThreadSafeQueue
{
public:
    void Push(const T& item)
    {
        std::lock_guard<std::mutex> lock(mtx);
        queue.push(item);
    }
    
    bool Pop(T& item)
    {
        std::lock_guard<std::mutex> lock(mtx);
        if (queue.empty())
        {
            return false;
        }
        item = queue.front();
        queue.pop();
        return true;
    }
    
    bool Empty() const
    {
        std::lock_guard<std::mutex> lock(mtx);
        return queue.empty();
    }

private:
    mutable std::mutex mtx;
    std::queue<T> queue;
};

int main()
{
    ThreadSafeQueue<int> queue;
    
    // 生产者线程
    std::thread producer([&queue]()
    {
        for (int i = 0; i < 10; ++i)
        {
            queue.Push(i);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    });
    
    // 消费者线程
    std::thread consumer([&queue]()
    {
        int value;
        // 注意:这个实现存在竞态条件,实际应用中应该使用条件变量
        // 这里仅作为简化示例,展示基本的线程安全队列操作
        while (queue.Pop(value))
        {
            std::cout << "消费: " << value << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
        }
    });
    
    producer.join();
    consumer.join();
    
    return 0;
}

7. 常见错误和注意事项

7.1 死锁

死锁是指两个或多个线程相互等待对方释放锁,导致程序无法继续执行。

7.1.1 死锁示例
cpp 复制代码
// ❌ 错误示例:死锁
std::mutex mtx1;
std::mutex mtx2;

void Thread1()
{
    mtx1.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟工作
    mtx2.lock();  // 等待 Thread2 释放 mtx2
    // 临界区代码
    mtx2.unlock();
    mtx1.unlock();
}

void Thread2()
{
    mtx2.lock();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟工作
    mtx1.lock();  // 等待 Thread1 释放 mtx1(死锁!)
    // 临界区代码
    mtx1.unlock();
    mtx2.unlock();
}
7.1.2 避免死锁的方法
  1. 统一加锁顺序:所有线程按照相同的顺序加锁
  2. 使用 std::lock:同时锁定多个互斥锁
  3. 避免嵌套锁:尽量减少锁的嵌套
  4. 使用超时锁 :使用 std::timed_mutex 设置超时

7.2 锁的粒度

锁的粒度应该尽可能小,只保护必要的代码:

cpp 复制代码
// ❌ 错误示例:锁的粒度过大
void BadFunction()
{
    std::lock_guard<std::mutex> lock(mtx);
    
    // DoWork1();  // 不需要锁的操作
    // DoWork2();  // 不需要锁的操作
    // DoCriticalWork();  // 需要锁的操作
    // DoWork3();  // 不需要锁的操作
}

// ✅ 正确示例:锁的粒度小
void GoodFunction()
{
    // DoWork1();  // 不需要锁的操作
    
    {
        std::lock_guard<std::mutex> lock(mtx);
        // DoCriticalWork();  // 只保护需要锁的操作
    }
    
    // DoWork2();  // 不需要锁的操作
    // DoWork3();  // 不需要锁的操作
}

7.3 不要在持有锁时调用用户代码

cpp 复制代码
#include <functional>
#include <mutex>

// ❌ 错误示例:在持有锁时调用用户代码
void BadFunction(std::function<void()> callback)
{
    std::lock_guard<std::mutex> lock(mtx);
    callback();  // 用户代码可能持有其他锁,导致死锁
}

// ✅ 正确示例:先调用用户代码,再加锁
void GoodFunction(std::function<void()> callback)
{
    callback();  // 先执行用户代码
    
    std::lock_guard<std::mutex> lock(mtx);
    // 临界区代码
}

7.4 不要忘记解锁

虽然使用 RAII 可以自动解锁,但在某些情况下仍需注意:

cpp 复制代码
// ❌ 错误示例:忘记解锁
void BadFunction()
{
    mtx.lock();
    // DoWork();
    // 忘记 unlock(),导致死锁
}

// ✅ 正确示例:使用 RAII
void GoodFunction()
{
    std::lock_guard<std::mutex> lock(mtx);
    // DoWork();
    // 自动解锁
}

8. 性能考虑

8.1 锁的开销

锁操作有一定的开销:

  • 系统调用开销:加锁和解锁可能涉及系统调用
  • 上下文切换:等待锁的线程可能被阻塞
  • 缓存失效:锁的竞争可能导致 CPU 缓存失效

8.2 减少锁竞争

  1. 减小锁的粒度:只保护必要的代码
  2. 使用无锁数据结构:对于简单操作,考虑使用原子操作
  3. 读写分离 :使用读写锁(std::shared_mutex,C++17)
  4. 减少共享数据:尽量减少线程间共享的数据

8.3 锁的性能对比

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <vector>

std::mutex mtx;
int counter = 0;

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

int main()
{
    const int threadCount = 4;
    const int incrementPerThread = 1000000;
    
    auto start = std::chrono::high_resolution_clock::now();
    
    std::vector<std::thread> threads;
    for (int i = 0; i < threadCount; ++i)
    {
        threads.emplace_back(IncrementWithLock, incrementPerThread);
    }
    
    for (auto& t : threads)
    {
        t.join();
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "耗时: " << duration.count() << " 毫秒" << std::endl;
    std::cout << "最终值: " << counter << std::endl;
    
    return 0;
}

9. 总结

std::mutex 是 C++11 提供的线程同步基础工具,正确使用可以保证多线程程序的数据安全。关键要点:

  1. 优先使用 RAII :使用 std::lock_guardstd::unique_lock 自动管理锁
  2. 避免死锁 :统一加锁顺序,或使用 std::lock 同时锁定多个锁
  3. 减小锁粒度:只保护必要的代码,减少锁竞争
  4. 异常安全:使用 RAII 确保异常时也能正确释放锁
  5. 性能考虑:在保证正确性的前提下,尽量减少锁的使用

记住:正确性永远比性能更重要。只有在确保程序正确运行后,才应该考虑性能优化。

相关推荐
朔北之忘 Clancy2 小时前
2025 年 9 月青少年软编等考 C 语言一级真题解析
c语言·开发语言·c++·学习·数学·青少年编程·题解
量子炒饭大师2 小时前
【C++入门】Cyber底码作用域的隔离协议——【C++命名空间】(using namespace std的原理)
开发语言·c++·dubbo
REDcker3 小时前
RTCP 刀尖点跟随技术详解
c++·机器人·操作系统·嵌入式·c·数控·机床
楚Y6同学3 小时前
基于 Haversine 公式实现【经纬度坐标点】球面距离计算(C++/Qt 实现)
开发语言·c++·qt·经纬度距离计算
老歌老听老掉牙3 小时前
优化样条曲线拟合参数解决三维建模中的截面连续性问题
c++·opencascade·样条曲线
散峰而望4 小时前
【算法竞赛】栈和 stack
开发语言·数据结构·c++·算法·leetcode·github·推荐算法
不爱吃糖的程序媛4 小时前
OpenHarmony 通用C/C++三方库 标准化鸿蒙化适配
c语言·c++·harmonyos
fqbqrr4 小时前
2601C++,导出控制
c++
陌路204 小时前
日志系统7--异步日志的实现
c++