std::unique_lock<std::mutex> 硬核理解

通过"数到100"理解 std::mutexstd::unique_lock

多线程编程中,同步是绕不开的课题。当多个线程同时读写共享数据时,如果没有保护机制,结果往往是混乱的。

下面用一个最简单的例子——两个线程轮流数数——来直观感受 std::mutexstd::unique_lock 的作用。


问题:没有锁会发生什么?

假设两个线程同时对全局变量 num++num,没有同步措施:

// 危险代码:无锁保护
void increment() {
    for (int i = 0; i < 50; ++i) {
        ++num;  // 非原子操作!
        std::cout << num << "\n";
    }
}

++num 实际分三步:

  1. 读取 num 的值
  2. 值加 1
  3. 写回内存

两个线程可能同时读到相同的值,各自加 1 后写回,结果就是漏数重数

时间线 线程A 线程B 实际 num
T1 num=5 5
T2 num=5 5
T3 写回 6 6
T4 写回 6 6(应该是7)

50 + 50 次递增,最终结果可能只有 70、80,甚至更低。


解决:加锁保护

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

class Counter {
public:
    void increment(int times, int threadNo) {
        for (int i = 0; i < times; ++i) {
            // 构造时加锁,析构时自动解锁
            std::unique_lock<std::mutex> lock(mtx);
            
            ++num;  // 临界区:此时只有一个线程能访问 num
            
            std::cout << "Thread " << threadNo 
                      << ": " << num << "\n";
        } // lock 在这里销毁,自动释放 mtx
    }

    void run() {
        std::thread t1(&Counter::increment, this, 50, 1);
        std::thread t2(&Counter::increment, this, 50, 2);
        
        t1.join();
        t2.join();
        
        std::cout << "Final: " << num << "\n";  // 一定是 100
    }

private:
    std::mutex mtx;  // 互斥锁本体
    int num = 0;
};

std::mutex vs std::unique_lock 分工

组件 角色 职责
std::mutex 资源 被锁保护的对象,本身只提供 lock()/unlock()
std::unique_lock 管理者 封装 mutex,实现RAII(构造加锁、析构解锁)

为什么用 unique_lock 而不是直接操作 mutex

// 方式1:原始操作(容易出错)
mtx.lock();
++num;
// 如果这里抛出异常或提前 return,锁永远解不开了!
mtx.unlock();

// 方式2:RAII 管理(推荐)
{
    std::unique_lock<std::mutex> lock(mtx);  // 加锁
    ++num;
}  // 无论正常结束还是异常,这里必定解锁

运行效果

数数

的确不会乱,不会多数,也不会少数,但是细心的朋友会发现,这个要么是1号线程先数,要么是2号线程先数,那要轮换数数如何做呢?

可以实现交替数数(线程1 → 线程2 → 线程1 → 线程2...),但需要两个条件变量来互相唤醒。

核心思路

线程1: 数奇数 (1,3,5...)    线程2: 数偶数 (2,4,6...)
        ↓                         ↓
   数完唤醒2                  数完唤醒1
   自己等待                    自己等待

交替数数代码

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

class AlternatingCounter {
public:
    void increment(int threadNo, int target) {
        for (int i = 0; i < 50; ++i) {
            std::unique_lock<std::mutex> lock(mtx);
            
            // 等待轮到自己:thread1 数奇数,thread2 数偶数
            cv.wait(lock, [this, target] { 
                return num % 2 == target;  // target: 1=奇数轮, 0=偶数轮
            });
            
            ++num;
            std::cout << "Thread" << threadNo << ": " << num << std::endl;
            
            // 数完唤醒另一个线程
            cv.notify_one();
        }
    }

    void run() {
        std::thread t1(&AlternatingCounter::increment, this, 1, 1);  // 目标奇数
        std::thread t2(&AlternatingCounter::increment, this, 2, 0);  // 目标偶数
        
        t1.join();
        t2.join();
        std::cout << "Final: " << num << std::endl;
    }

private:
    std::mutex mtx;
    std::condition_variable cv;
    int num = 0;
};

预期输出效果

Thread1: 1   (奇数)
Thread2: 2   (偶数)
Thread1: 3   (奇数)
Thread2: 4   (偶数)
...
Thread1: 99
Thread2: 100
Final: 100

关键机制

组件 作用
num % 2 == target 判断轮次:thread1 等奇数,thread2 等偶数
cv.wait() 不满足条件就阻塞,释放锁让别人干活
cv.notify_one() 数完立即唤醒对方,自己下一轮再等待

对比

方式 特点 适用场景
单锁竞争(你原来的) 谁抢到谁数,顺序随机 高并发吞吐,不 care 顺序
双条件变量交替 严格轮询,顺序确定 需要协作、流水线、交替执行

注意:交替模式性能更低(频繁上下文切换),只适合需要严格顺序控制的场景。

运行效果

交替计数

这以下这段代码解析一下:

cv.wait(lock, [this, target] { 
                return num % 2 == target;  
            });

执行到这个地方,如果不符合条件,return false,那么这个线程就挂起了(如果不挂起就空转,导致cpu资源浪费),
然后其实这个时候的lock是解锁状态,当其他的地方发送了notify,然后这个地方才被唤醒,唤醒以后加个锁,锁上开始往下执行,
如果没有以下代码:

[this, target] { 
    return num % 2 == target;  
 }

那就得这样写了:

while (num % 2 == threadNo)
{
		cv.wait(lock);
}

因为这个地方又涉及到另外一个问题,“虚假唤醒”,感觉没完没了:)

虚假唤醒

什么是虚假唤醒(spurious wakeup):线程可能在没有被 notify 的情况下,从 wait() 中醒来
为什么会这样? 是操作系统和硬件层面的特性,不是 C++ 特有的问题

原因 说明
操作系统实现 POSIX 标准允许 pthread_cond_wait 不保证只被信号唤醒
信号中断 Unix 信号(如 SIGINT)可能导致线程提前返回
多核 CPU 优化 某些架构为了性能,选择"宁可错杀"的策略
库/内核简化 实现方觉得严格保证"只被 notify 唤醒"成本太高

为什么标准允许虚假唤醒?

权衡 说明
性能 严格保证"只被 notify 唤醒"需要更多同步开销
复杂度 内核实现更简单、更高效
可移植性 不同平台行为统一(都可能有虚假唤醒)

后记

通过这个篇幅,是不是对多线程处理有深刻的认识,如果对您有帮助,帮忙关注走一波!

posted @ 2024-12-04 17:51  Tlink  阅读(556)  评论(0)    收藏  举报