一、核心概念类比

想象一个公司里的三个角色:

  • 老板:负责分配任务(主线程)
  • 员工:执行具体工作(子线程)
  • 任务看板:传递任务信息的共享区域(条件变量和锁)

二、三个关键函数的作用

1. wait()
  • 员工行为:员工发现当前没有任务(队列空),于是在看板前睡觉,同时把看板钥匙交给前台保管。
  • 代码逻辑
    if (没有任务) {
        放下看板钥匙;  // 释放锁
        开始睡觉;    // 进入wait状态
        拿起看板钥匙; // 被唤醒后重新获取锁
    }
    
  • 关键点
    • 原子性:放下钥匙和睡觉必须同时完成,避免其他员工在中间插入新任务而错过通知
    • 自动唤醒:当老板在看板上贴了新任务并通知后,员工会自动醒来并重新拿起钥匙
2. notify_one()
  • 老板行为:老板在看板上贴了新任务后,拍了拍正在睡觉的某个员工肩膀。
  • 代码逻辑
    把新任务写到看板上;  // 修改共享数据
    拍醒一个睡觉的员工; // 调用notify_one()
    
  • 关键点
    • 无需持有钥匙:老板不需要拿看板钥匙就能拍醒员工
    • 只唤醒一个:如果有多个员工在睡觉,只随机唤醒一个
3. join()
  • 老板行为:老板在下班前,必须确认某个员工已经完成所有工作并离开公司。
  • 代码逻辑
    等待员工完成工作; // 调用join()
    下班回家;       // 主线程继续执行
    
  • 关键点
    • 阻塞老板:老板必须等待,不能提前下班
    • 资源回收:员工离开时会清理自己的工作台(释放线程资源)

三、三者的协作流程

场景:餐厅点餐系统
  1. 顾客下单(主线程):

    {
        加锁;                  // 拿起菜单本(互斥锁)
        把订单写到菜单本上;    // 修改共享队列
        解锁;                  // 放下菜单本
        通知厨师;              // notify_one()
    }
    
  2. 厨师等待订单(子线程):

    while (餐厅营业中) {
        加锁;                  // 拿起菜单本
        if (菜单本是空的) {
            解锁并等待;        // wait():放下菜单本并睡觉
            加锁;              // 被唤醒后重新拿起菜单本
        }
        处理订单;             // 读取共享队列
        解锁;                 // 放下菜单本
    }
    
  3. 餐厅打烊(主线程结束前):

    关闭餐厅;                // 设置终止标志
    通知所有厨师;            // notify_all()
    等待所有厨师下班;        // 对每个厨师线程调用join()
    锁门离开;                // 主线程退出
    

四、实现原理(简化版)

1. wait()的底层逻辑
// 伪代码表示wait()的实现
void wait(std::unique_lock<std::mutex>& lock) {
    1. 记录当前线程ID到等待队列;
    2. 原子性操作:
       - 释放锁;
       - 将线程状态设为"等待";
       - 切换到内核态让线程休眠;
    3. 被唤醒后:
       - 重新获取锁;
       - 从等待队列移除线程ID;
}
2. notify_one()的底层逻辑
// 伪代码表示notify_one()的实现
void notify_one() {
    1. 检查等待队列是否有线程;
    2. 如果有:
       - 从队列中取出一个线程ID;
       - 将该线程状态设为"就绪";
       - 通知操作系统唤醒该线程;
    3. 返回; // 不持有锁,不关心唤醒的线程何时执行
}
3. join()的底层逻辑
// 伪代码表示join()的实现
void join() {
    1. 检查线程是否已结束;
    2. 如果未结束:
       - 将当前线程(主线程)设为"等待该子线程";
       - 让出CPU时间,进入休眠;
    3. 当子线程结束时:
       - 操作系统唤醒等待的主线程;
       - 回收子线程资源(栈、线程ID等);
    4. 返回; // 子线程已安全结束
}

五、常见误区与最佳实践

1. 误区
  • 认为notify_one()会立即唤醒线程:实际上只是标记线程可被唤醒,具体执行时机由操作系统调度
  • 忘记在join()前通知线程:如果线程阻塞在wait(),不通知会导致join()永久等待
  • 使用notify_one()代替join():两者作用完全不同,不能互相替代
2. 最佳实践
  • 终止线程三部曲
    设置终止标志;     // 如keep_running = false
    notify_one();   // 唤醒可能的等待
    join();         // 等待线程退出
    
  • 永远在循环中wait
    while (条件不满足) {
        wait();
    }
    
  • 优先使用带谓词的wait
    wait(lock, []{ return 条件满足; }); // 自动处理循环和唤醒检查
    

六、总结

  • wait():线程释放锁并休眠,等待通知
  • notify_one():唤醒一个等待中的线程(不释放锁)
  • join():等待线程执行完毕并回收资源

三者结合实现了线程间的同步与协作,是C++多线程编程的核心机制。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐