目录

引言

为什么我们需要互斥锁?

互斥锁的局限性:

条件变量:让线程学会“睡觉”

为什么条件变量必须配合互斥锁?

实战演练:生产者-消费者模型

常见误区与最佳实践

总结


引言

在多线程编程的世界里,我们经常会遇到这样的场景:多个线程同时操作一个共享变量(比如全局计数器),结果数据乱了;或者一个线程需要等待另一个线程完成某项任务后才能继续执行。

如果处理不好,你的程序可能会出现数据竞争、CPU 占用率飙升,甚至直接卡死(死锁)。今天,我们就来深入聊聊解决这些问题的两把“钥匙”——互斥锁(Mutex)条件变量(Condition Variable)

为什么我们需要互斥锁?

想象一下,你和你的同事共用一个记事本记账。如果你们同时拿起笔,在同一行写数字,结果会怎样?字迹重叠,账目混乱。在计算机中,这就是竞态条件(Race Condition)

互斥锁(Mutex)就像是一把“厕所钥匙”。

  • 加锁(Lock):谁拿到了钥匙,谁就能进去上厕所(访问共享资源)。
  • 解锁(Unlock):上完厕所出来,把钥匙挂回去,下一个人才能进去。
  • 阻塞:如果钥匙被别人拿走了,你就得在门口等着,啥也干不了。

在 C++ 中,我们通常这样使用:

#include <mutex>

std::mutex mtx; // 定义一把锁
int counter = 0; // 共享资源

void safeIncrement() {
    mtx.lock();   // 1. 进门,锁门
    counter++;    // 2. 安全地修改数据
    mtx.unlock(); // 3. 出门,解锁
}

最佳实践:RAII 锁守卫

手动 lock 和 unlock 有个风险:如果在临界区代码中发生了异常或提前 return,忘记解锁会导致死锁。
现代 C++ 推荐使用 std::lock_guard 或 std::unique_lock,它们利用 RAII 机制,在对象销毁时自动解锁:

void safeIncrement() {
    std::lock_guard<std::mutex> lock(mtx); // 作用域结束自动解锁
    counter++;
}

互斥锁的局限性:

互斥锁只能保证“互斥访问”,但它无法解决“等待条件”的问题。比如,消费者线程想从队列里取数据,但队列是空的。如果只用互斥锁,消费者只能不断地加锁、检查、解锁、再加锁……这种“忙等待(Busy Waiting)”会把 CPU 跑满,效率极低。

条件变量:让线程学会“睡觉”

为了解决“忙等待”,我们需要一种机制:当条件不满足时,线程主动去睡觉(释放 CPU);当条件满足时,另一个线程把它叫醒。 这就是条件变量(Condition Variable)。

条件变量通常与互斥锁配合使用,它主要包含两个动作:

  1. 等待(Wait):如果条件不满足,就释放锁并进入睡眠。
  2. 通知(Notify):改变条件后,唤醒正在睡眠的线程。

为什么条件变量必须配合互斥锁?

很多初学者会问:“为什么 wait 函数必须传入一个互斥锁?直接等不行吗?”

这主要是为了解决原子性竞态条件的问题。

想象一个没有锁保护的糟糕场景:

  1. 消费者线程检查队列,发现是空的(准备去睡觉)。
  2. 就在这一瞬间! 生产者线程往队列里放入了数据,并发送了“唤醒信号”。
  3. 消费者线程没收到信号(因为它还没睡着),接着调用了 wait() 进入死睡。
  4. 结果:数据已经在队列里了,但消费者永远在等下一个信号,程序卡死。

加上互斥锁后的标准流程(原子操作):
cond.wait(lock) 这个函数内部其实做了三件连贯的事:

  1. 释放锁:让其他线程(生产者)有机会获取锁并修改数据。
  2. 阻塞等待:线程进入睡眠状态,不占用 CPU。
  3. 被唤醒后重新加锁:当被 notify 唤醒时,它会自动尝试重新获取锁,成功后才从 wait 函数返回。

这种机制保证了“检查条件”和“进入等待”这两个动作的原子性,彻底杜绝了信号丢失。

实战演练:生产者-消费者模型

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

std::queue<int> q;             // 共享的任务队列
std::mutex mtx;                // 互斥锁
std::condition_variable cv;    // 条件变量
const int MAX_SIZE = 10;       // 队列最大容量
bool finished = false;         // 结束标志

// 生产者线程
void producer() {
    for (int i = 0; i < 20; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 如果队列满了,就等待消费者取走数据
        cv.wait(lock, []{ return q.size() < MAX_SIZE; });
        
        q.push(i);
        std::cout << "生产了: " << i << " (当前队列大小: " << q.size() << ")\n";
        
        lock.unlock(); // 通知前解锁是个好习惯,减少锁竞争
        cv.notify_all(); // 唤醒所有等待的线程(包括消费者)
        
        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时
    }
    
    // 生产结束,通知消费者退出
    {
        std::lock_guard<std::mutex> lock(mtx);
        finished = true;
    }
    cv.notify_all();
}

// 消费者线程
void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        
        // 等待队列非空,或者生产结束
        //因为 finished 为 true 时队列可能还有未消费完的数据,需要消费完再退出
        cv.wait(lock, []{ return !q.empty() || finished; });
        
        // 如果队列空了且生产已结束,退出循环
        if (q.empty() && finished) break;
        
        int val = q.front();
        q.pop();
        std::cout << "消费了: " << val << " (当前队列大小: " << q.size() << ")\n";
        
        lock.unlock();
        cv.notify_all(); // 唤醒生产者(因为队列有空位了)
        
        std::this_thread::sleep_for(std::chrono::milliseconds(150));
    }
}

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

常见误区与最佳实践

在实战中,有几个细节如果不注意,很容易写出 Bug:

  1. 警惕“虚假唤醒(Spurious Wakeup)”
    操作系统底层可能会在没有收到 notify 的情况下意外唤醒线程。

    • 错误写法if (condition) cv.wait(lock);
    • 正确写法while (!condition) cv.wait(lock);
      或者使用 C++ 提供的带谓词的版本:cv.wait(lock, []{ return condition; });,它内部就是一个 while 循环。
  2. notify_one() 还是 notify_all()?

    • notify_one():只唤醒一个等待线程。适用于资源只有一个(如队列里只有一个任务),唤醒多了也是浪费。
    • notify_all():唤醒所有等待线程。适用于状态发生全局改变(如程序退出、资源批量就绪)。
  3. 锁的粒度控制
    不要把耗时的操作(如 I/O、复杂计算)放在锁里面。锁住的代码越少,并发性能越高。

总结

互斥锁和条件变量是多线程同步的“黄金搭档”。

  • 互斥锁负责保护共享数据,确保同一时间只有一个线程能修改它。
  • 条件变量负责线程间的通信,让线程在条件不满足时“优雅地睡觉”,条件满足时“及时地工作”。

掌握这两个工具,你就掌握了并发编程的半壁江山。建议大家把上面的生产者-消费者代码敲一遍,体会一下锁的获取与释放、线程的阻塞与唤醒,相信你会有更深的理解!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐