【多线程】阻塞等待(Blocking Wait)(以C++为例)
【多线程】阻塞等待(Blocking Wait)(以C++为例)
本文来自于我关于多线程的系列文章。欢迎阅读、点评与交流
1.【多线程】互斥锁(Mutex)是什么?
2.【多线程】临界区(Critical Section)是什么?
3.【多线程】计算机领域中的各种锁
4.【多线程】信号量(Semaphore)是什么?
5.【多线程】信号量(Semaphore)常见的应用场景
6.【多线程】条件变量(Condition Variable)是什么?
7.【多线程】监视器(Monitor)是什么?
8.【多线程】什么是原子操作(Atomic Operation)?
9.【多线程】竞态条件(race condition)是什么?
10.【多线程】无锁数据结构(Lock-Free Data Structures)是什么?
11.【多线程】线程休眠(Thread Sleep)的底层实现
12.【多线程】多线程的底层实现
13.【多线程】读写锁(Read-Write Lock)是什么?
14.【多线程】死锁(deadlock)
15.【多线程】线程池(Thread Pool)
16.【多线程】忙等待/自旋(Busy Waiting/Spinning)
17.【多线程】阻塞等待(Blocking Wait)(以Java为例)
18.【多线程】阻塞等待(Blocking Wait)(以C++为例)
19.【多线程】屏障(Barrier)
20.【多线程硬件机制】总线锁(Bus Lock)是什么?
21.【多线程硬件机制】缓存锁(Cache Lock)是什么?
阻塞等待(Blocking Wait) 是一个并发编程中的核心概念之一。
1. 什么是阻塞等待?
阻塞等待指的是一个线程在执行过程中,由于某些条件暂时不满足(例如等待I/O操作完成、等待获取锁、等待另一个线程的结果等),而主动或被动地暂停自己的执行,让出CPU资源,进入一种“休眠”状态。直到它所等待的条件被满足后,才会被唤醒,重新进入就绪状态,等待CPU调度继续执行。
简单来说就是:线程停下来,等某个事情发生。
2. 为什么需要阻塞等待?
如果没有阻塞等待机制,线程在条件不满足时只能不停地循环检查(即“忙等待”或“自旋”),这会白白浪费宝贵的CPU时间片。
对比一下:
-
阻塞等待:
- 线程:”锁还没释放?那我先睡了,锁释放了记得叫醒我。“
- 优点: 不占用CPU,节能高效。
- 缺点: 线程切换会带来一定的上下文切换开销。
-
忙等待:
- 线程:”锁还没释放?我查一下…还没…我再查一下…还没…“
- 优点: 响应及时,一旦条件满足可立即继续。
- 缺点: 持续占用CPU,浪费资源,可能导致性能问题。
在绝大多数应用场景下,阻塞等待是更优的选择,因为CPU时间是宝贵的,应该留给真正需要计算的线程。
3. 常见的阻塞等待场景(附C++代码示例)
以下是一些在C++中典型的会导致线程阻塞等待的情况。
场景一:互斥锁(std::mutex)
当线程尝试获取已被其他线程持有的互斥锁时,会发生阻塞等待。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>std::mutex g_mutex;void worker(int id) {std::lock_guard<std::mutex> lock(g_mutex); // 尝试获取锁,如果被占用则阻塞等待std::cout << "线程 " << id << " 获取到锁,开始执行" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作std::cout << "线程 " << id << " 释放锁" << std::endl;// lock_guard析构时自动释放锁
}int main() {std::thread t1(worker, 1);std::thread t2(worker, 2);t1.join();t2.join();return 0;
}
输出:
线程 1 获取到锁,开始执行
(等待约2秒后)
线程 1 释放锁
线程 2 获取到锁,开始执行
线程 2 释放锁
场景二:条件变量(std::condition_variable)
条件变量允许线程在某个条件不满足时主动等待,直到其他线程通知条件发生变化。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>std::mutex g_mutex;
std::condition_variable g_cv;
bool g_ready = false;void waiter() {std::unique_lock<std::mutex> lock(g_mutex);std::cout << "等待线程:检查条件,条件不满足,开始等待" << std::endl;// 等待条件满足(防止虚假唤醒)g_cv.wait(lock, []{ return g_ready; });std::cout << "等待线程:条件满足,继续执行" << std::endl;
}void notifier() {std::this_thread::sleep_for(std::chrono::seconds(1));{std::lock_guard<std::mutex> lock(g_mutex);std::cout << "通知线程:改变条件并通知" << std::endl;g_ready = true;}g_cv.notify_one(); // 唤醒一个等待的线程std::cout << "通知线程:通知已完成" << std::endl;
}int main() {std::thread t1(waiter);std::thread t2(notifier);t1.join();t2.join();return 0;
}
场景三:线程等待(std::thread::join)
主线程等待子线程执行完成。
#include <iostream>
#include <thread>
#include <chrono>void worker() {std::cout << "工作线程开始工作..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "工作线程完成工作" << std::endl;
}int main() {std::thread t(worker);std::cout << "主线程等待工作线程完成..." << std::endl;t.join(); // 主线程阻塞等待工作线程结束std::cout << "主线程继续执行" << std::endl;return 0;
}
场景四:带超时的等待
C++提供了带超时机制的等待,避免无限期阻塞。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>std::mutex g_mutex;
std::condition_variable g_cv;
bool g_ready = false;void timed_waiter() {std::unique_lock<std::mutex> lock(g_mutex);std::cout << "定时等待线程:开始等待,最多等3秒" << std::endl;auto status = g_cv.wait_for(lock, std::chrono::seconds(3), []{ return g_ready; });if (status) {std::cout << "定时等待线程:条件在超时前满足" << std::endl;} else {std::cout << "定时等待线程:等待超时,条件仍未满足" << std::endl;}
}void slow_notifier() {std::this_thread::sleep_for(std::chrono::seconds(5)); // 5秒后才通知{std::lock_guard<std::mutex> lock(g_mutex);g_ready = true;}g_cv.notify_one();std::cout << "慢速通知线程:通知已发出" << std::endl;
}int main() {std::thread t1(timed_waiter);std::thread t2(slow_notifier);t1.join();t2.join();return 0;
}
场景五:Future和Promise(std::future, std::promise)
用于在线程间传递结果,等待异步操作完成。
#include <iostream>
#include <thread>
#include <future>
#include <chrono>int heavy_computation() {std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "计算完成" << std::endl;return 42;
}int main() {// 异步执行计算std::future<int> result = std::async(std::launch::async, heavy_computation);std::cout << "主线程可以做其他事情..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "主线程其他事情做完,等待计算结果..." << std::endl;// 阻塞等待并获取结果int value = result.get();std::cout << "计算结果: " << value << std::endl;return 0;
}
4. C++线程状态与阻塞
虽然C++标准没有像Java那样明确定义线程状态,但在实际实现中,线程在阻塞等待时会处于类似的状态:
- 阻塞在互斥锁上:等待获取锁
- 等待条件变量:主动暂停执行
- 等待join:等待其他线程结束
- 等待future:等待异步操作结果
5. 阻塞等待 vs 忙等待(自旋锁)
#include <iostream>
#include <thread>
#include <atomic>
#include <chrono>// 忙等待实现的自旋锁
class SpinLock {
private:std::atomic<bool> locked{false};
public:void lock() {while (locked.exchange(true, std::memory_order_acquire)) {// 忙等待:持续检查直到锁可用}}void unlock() {locked.store(false, std::memory_order_release);}
};SpinLock spin_lock;
std::mutex normal_mutex;void spin_lock_worker(int id) {spin_lock.lock(); // 忙等待获取锁std::cout << "自旋锁线程 " << id << " 开始" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "自旋锁线程 " << id << " 结束" << std::endl;spin_lock.unlock();
}void normal_lock_worker(int id) {std::lock_guard<std::mutex> lock(normal_mutex); // 阻塞等待获取锁std::cout << "普通锁线程 " << id << " 开始" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));std::cout << "普通锁线程 " << id << " 结束" << std::endl;
}
总结
特性 | C++阻塞等待 | C++忙等待 |
---|---|---|
CPU占用 | 不占用CPU | 持续占用CPU |
实现方式 | std::mutex , std::condition_variable , future::get() | 自旋锁,原子变量循环检查 |
响应速度 | 较慢(需要线程切换) | 很快(立即响应) |
适用场景 | 大多数高并发场景,I/O操作 | 极短等待,低竞争场景 |
C++提供了丰富的同步机制来实现阻塞等待,正确使用这些机制可以编写出高效、安全的并发程序。选择阻塞等待还是忙等待取决于具体的性能要求和应用场景。