C++ 多线程 wait,notifty,mutex 的使用

  • 阻塞

    • 当进程需要等待某个事件时,它会主动或被动地进入阻塞状态
    • 阻塞的进程会释放 CPU,操作系统会将 CPU 分配给其他就绪的进程。
    • 打个比方。在Breaking Bad中,每位家庭成员坐在一起谈心。为了交谈的高效性,同一时刻只允许一位家庭成员发言(类比:进程互斥)。所以弄了个抱枕(类比:锁)放在桌子上,要发言的人只有拿到抱枕(获取锁)才能发言。发言完毕后,将抱枕放回到桌子上(释放锁)
    • 锁的使用
      • std::mutex
        #include <iostream>
        #include <thread>
        #include <mutex>
        	
        std::mutex mtx; // 全局互斥锁
        int sharedData = 0; // 共享数据
        
        void increment() {
            for (int i = 0; i < 100000; ++i) {
                mtx.lock(); // 加锁
                ++sharedData; // 访问共享数据
                mtx.unlock(); // 解锁
            }
        }
        	
        int main() {
            std::thread t1(increment);
            std::thread t2(increment);
        
            t1.join();
            t2.join();
        
            std::cout << "Final value: " << sharedData << std::endl; // 输出: 200000
            return 0;
        }
        
        • 这种方式,类似于裸指针,每次必须手动调用 lock()unlock(),否则可能导致死锁或资源泄漏。
        • 如果临界区代码抛出异常,unlock() 可能不会被调用,导致死锁。(占着不释放)
      • std::lock_guard
        #include <iostream>
        #include <thread>
        #include <mutex>
        
        std::mutex mtx; // 全局互斥锁
        int sharedData = 0; // 共享数据
        
        void increment() {
            for (int i = 0; i < 100000; ++i) {
                std::lock_guard<std::mutex> lock(mtx); // 自动加锁和解锁
                ++sharedData; // 访问共享数据
            }
        }
        
        int main() {
            std::thread t1(increment);
            std::thread t2(increment);
        
            t1.join();
            t2.join();
        
            std::cout << "Final value: " << sharedData << std::endl; // 输出: 200000
            return 0;
        }
        
        • 这种RAII风格的锁管理器,在构造时自动加锁,在析构时自动解锁,避免了手动管理锁的麻烦
      • std::unique_lock
        #include <iostream>
        #include <thread>
        #include <mutex>
        
        std::mutex mtx; // 全局互斥锁
        int sharedData = 0; // 共享数据
        
        void increment() {
            for (int i = 0; i < 100000; ++i) {
                std::unique_lock<std::mutex> lock(mtx); // 自动加锁和解锁
                ++sharedData; // 访问共享数据
            }
        }
        
        int main() {
            std::thread t1(increment);
            std::thread t2(increment);
        
            t1.join();
            t2.join();
        
            std::cout << "Final value: " << sharedData << std::endl; // 输出: 200000
            return 0;
        }
        
  • 在 C++ 多线程编程中,线程间的协调是一个经常需要考虑的问题。我们经常需要一个线程等待某个条件满足(例如,等待任务队列非空,或等待某个计算完成),而另一个线程则负责在条件满足时通知等待的线程。

    • 这么说依然抽象,现在就假设在消费者-生产者问题下。引入锁机制只是解决了对临界资源(即产品队列)的互斥操作,使得同一时刻只有一个进程访问互斥资源。
    • 假设现在一个消费者进程A拿到锁了并在CPU上运行,CPU分配给他的时间片有100ms。假设此时产品队列为空,那么进程A什么都干不了,只有空转。比如它花了10ms判断到产品队列为空,剩下90ms它将无所事事,相当于浪费了这个时间片90%的效率。这就是忙等待或自旋的劣势。
    • 代码:
      // --- 极简化的伪代码,仅用于说明概念 ---
      std::mutex queue_mutex;
      std::queue<Task> task_queue;
      bool running = true;
      
      void consumer_thread() {
          while (running) {
              std::lock_guard<std::mutex> lock(queue_mutex); // 锁住队列
              if (!task_queue.empty()) {
                  Task task = task_queue.front();
                  task_queue.pop();
                  // ... process task ...
              } else {
                  // 队列为空,解锁并稍等片刻?
                  // 这就是问题所在!我们不想空转浪费 CPU!
              }
              // lock_guard 在离开作用域时自动解锁
          }
      }
      
  • 一个很直白的思路就是,对于忙等待(占着cpu又无所事事)的进程,应该让它阻塞。再次回顾阻塞的含义:释放 CPU并进入阻塞进程队列,等待将来的唤醒

    • 怎样“释放 CPU并进入阻塞进程队列”: wait函数
    • 谁来唤醒:另一个进程调用notify函数
  • void wait(std::unique_lock<std::mutex>& lock, Predicate pred):

    • lock:一个 std::unique_lock<std::mutex> 对象,用于保护共享资源
    • pred:一个可调用对象(如 Lambda 表达式),用于检查条件是否满足。如果谓词为 true,说明条件已经满足,wait 函数直接返回。线程继续执行,lock 仍然保持锁定状态。
    • 工作流程
      • 持有锁检查谓词: wait 函数首先检查你提供的 predicate (通常是一个 lambda 表达式)。此时,你传入的 lock 必须是锁定的状态。
      • 如果谓词为 true: 说明条件已经满足,wait 函数直接返回。线程继续执行,lock 仍然保持锁定状态。
      • 如果谓词为 false: 说明条件不满足,线程需要等待。此时 wait 执行一个关键的原子操作序列: a. 释放锁: wait自动地、原子地调用 lock.unlock(),释放掉你传入的 lock 所管理的互斥锁 (queue_mutex)。这是至关重要的一步,它允许其他线程(比如生产者)能够获取这个锁,进而修改共享状态(队列)并最终满足条件。 b. 阻塞线程: 当前线程进入阻塞(睡眠)状态,等待被 notify_one()notify_all() 唤醒。
      • 被唤醒: 当线程被 notify 或发生 spurious wakeup(虚假唤醒,这是可能发生的)时,它会从阻塞状态醒来。
      • 重新获取锁: 在唤醒后,wait 函数自动地、原子地尝试重新调用 lock.lock() 获取之前释放的互斥锁。线程可能会在这里再次阻塞,直到成功获取锁为止。
      • 再次检查谓词: 成功重新获取锁后,wait再次检查 predicate。
      • 如果 predicate 现在返回 true,wait 函数返回,线程继续执行。lock 此时是锁定的。
      • 如果 predicate 仍然返回 false(可能是虚假唤醒,或者条件被其他线程改变了),wait 不会返回,而是重复步骤 3a,再次释放锁并进入阻塞状态,等待下一次唤醒。
  • notify_all

    • 唤醒所有等待的线程。
  • 例子:考虑经典的消费者-生产者模型

    	#include <iostream>
    	#include <thread>
    	#include <queue>
    	#include <mutex>
    	#include <condition_variable>
    	
    	std::mutex mtx;
    	std::condition_variable cv;
    	std::queue<int> dataQueue;
    	
    	void producer() {
    	    for (int i = 0; i < 10; ++i) {
    	        std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时
    	        {
    	            std::unique_lock<std::mutex> lock(mtx);
    	            dataQueue.push(i);
    	            std::cout << "Produced: " << i << std::endl;
    	        }
    	        cv.notify_all(); // 通知消费者
    	    }
    	}
    	
    	void consumer() {
    	    while (true) {
    	        std::unique_lock<std::mutex> lock(mtx);
    	        cv.wait(lock, [] { return !dataQueue.empty(); }); // 等待队列不为空
    	        int value = dataQueue.front();
    	        dataQueue.pop();
    	        std::cout << "Consumed: " << value << std::endl;
    	        if (value == 9) break; // 结束条件
    	    }
    	}
    	
    	int main() {
    	    std::thread t1(producer);
    	    std::thread t2(consumer);
    	
    	    t1.join();
    	    t2.join();
    	
    	    return 0;
    	}
    	  ```
    
  • 类比:所以你可以想象到这样一个场景:每位家庭成员尝试获取抱枕并发言,对于拿到抱枕却又支支吾吾的,他应该立即放下抱枕让给别人。

  • 虚假唤醒:在不同的语言,甚至不同的操作系统上,条件锁都会产生虚假唤醒现象。所有语言的条件锁库都推荐用户把wait()放进循环里:

    while (!cond) {
        lock.wait();
    }
    
    • 由于线程调度的原因,被条件变量唤醒的线程在本线程内真正执行「加锁并返回」前,另一个线程插了进来,完整地进行了一套「拿锁、改条件、还锁」的操作。比如任务队列未空,多个消费者进程被唤醒,当一个消费者进程拿走了最后一个任务后,其他被唤醒的消费者如果不检查条件,就会出错。
    • 这也是推荐使用wait带谓词的重载版本,这样就包含了条件判断,而不需要在外面包一个循环体。
  • 参考资料
    深入理解 C++ 条件变量:为何 wait 钟爱 std::unique_lock?
    C++ std::condition_variable wait() wait_for() 区别 怎么用 实例
    虚假唤醒

<think>嗯,用户问的是C++多线程中的wait,需要详细解释一下。首先,我得回忆一下C++多线程相关的知识,尤其是条件变量和wait的用法。用户可能刚接触多线程,需要从基础讲起,但也不能太啰嗦。 首先,我应该明确wait的作用,它通常和条件变量一起使用,用于线程的等待和唤醒。然后,需要解释wait的两种形式:带谓词和不带谓词的。用户可能不太清楚为什么会有这两种形式,或者在使用时容易出错,比如虚假唤醒的问题。这时候需要用例子来说明,比如生产者-消费者模型,这样用户更容易理解。 记得要提到std::condition_variable,以及它和mutex的关系。必须强调在使用wait前需要获取锁,这是常见的错误点。另外,虚假唤醒的概念很重要,很多人在不理解的情况下会忽略,导致程序出现难以调试的问题。 可能用户在实际开发中遇到了死锁或者线程不唤醒的情况,需要给出正确的使用模式。比如在生产者-消费者中,如何正确地使用wait和notify。还有,带谓词的wait可以避免虚假唤醒,这样代码更简洁安全。 要确保回答结构清晰,分点解释,用例子辅助说明。同时,注意使用中文,避免术语混淆。可能需要检查一下C++标准的版本,比如C++11引入的多线程支持,但用户可能已经知道,所以不需要特别强调,除非有版本差异需要注意。 最后,总结一下使用wait的注意事项,比如锁的管理、条件检查、性能影响等,帮助用户避免常见错误。整个过程需要逻辑连贯,逐步引导用户理解wait的工作原理和正确用法。</think>在 C++ 多线程编程中,`wait` 是条件变量(`std::condition_variable`)的核心操作之一,用于实现线程间的同步协作。以下分步骤解析其工作机制: --- ### 一、基础概念 1. **条件变量** (`std::condition_variable`) - 用于阻塞一个或多个线程,直到: - 收到其他线程的通知(`notify_one()`/`notify_all()`) - 满足特定条件(通过谓词判断) 2. **与互斥锁的关系** - `wait` 必须与 `std::mutex` 配合使用 - 调用 `wait` 前需通过 `std::unique_lock<std::mutex>` 持有锁 --- ### 二、`wait` 的两种形式 #### 1. 基础形式(需手动检查条件) ```cpp std::unique_lock<std::mutex> lock(mutex); condition_var.wait(lock); ``` - **行为**: - 原子操作:释放锁 → 阻塞线程 → 被唤醒后重新获取锁 - 需在循环中手动检查条件,防止虚假唤醒 #### 2. 带谓词的形式(推荐) ```cpp condition_var.wait(lock, [&]{ return data_ready; }); ``` - **优势**: - 自动检查条件,避免虚假唤醒 - 等价于: ```cpp while (!predicate()) { wait(lock); } ``` --- ### 三、典型应用场景(生产者-消费者模型) ```cpp std::queue<int> buffer; std::mutex mtx; std::condition_variable cv; // 生产者线程 void producer() { std::unique_lock lock(mtx); buffer.push(42); cv.notify_one(); // 通知消费者 } // 消费者线程 void consumer() { std::unique_lock lock(mtx); cv.wait(lock, [&]{ return !buffer.empty(); }); // 等待非空条件 int data = buffer.front(); buffer.pop(); } ``` --- ### 四、关键注意事项 1. **虚假唤醒**(Spurious Wakeup) - 线程可能无故被唤醒,必须通过条件检查防御 2. **锁管理** - `wait` 会自动释放/重新获取锁,避免死锁 - 不要手动调用 `lock()`/`unlock()` 3. **性能影响** - 频繁唤醒可能影响性能,可结合超时机制: ```cpp cv.wait_for(lock, 100ms, predicate); ``` --- ### 五、底层原理 当调用 `wait` 时: 1. 线程进入等待队列 2. 释放关联的互斥锁 3. 被唤醒后重新竞争锁 4. 检查条件决定是否继续执行 --- ### 六、常见错误 ```cpp // 错误示例:未加锁调用 wait std::mutex mtx; cv.wait(mtx); // 编译错误! // 正确形式 std::unique_lock lock(mtx); cv.wait(lock); ``` 通过合理使用 `wait` 可以构建高效的多线程协作模型,但需特别注意线程安全和条件检查的严谨性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值