目录
前言
在多线程编程中,条件变量(
condition_variable
)是用来让一个线程等待某个条件满足时被唤醒的机制。而使用条件变量时,需要特别注意其用法和一些潜在问题,尤其是 信号丢失 和 原子性 问题。
信号丢失问题:如果没有互斥锁保护,
signal
可能会在wait
之前被调用,这会导致等待线程错过信号并永远等待下去。原子性保证:通过使用互斥锁,可以确保条件判断和
wait
操作的原子性。这样,即使signal
和wait
的调用顺序错乱,也不会造成线程死锁或条件变量失效的问题。
一、条件变量中的两个关键操作
wait
:一个线程调用wait
,表示这个线程要等待某个条件的满足。它会阻塞当前线程,直到收到其他线程的通知或条件满足。signal
或notify
:另一个线程在条件满足后调用signal
(或notify
),通知等待的线程条件已经满足,唤醒等待中的线程。
二、信号丢失问题
当 signal
发生在 wait
之前,可能会造成问题:如果 signal
先发出,而等待线程尚未进入 wait
状态,那么这个 signal
会被丢失。这是因为条件变量的 signal
只唤醒已经进入 wait
状态的线程,如果线程还没有进入 wait
,信号就会丢失,导致线程永远不会醒来。
三、原子性问题
wait
过程中需要确保 条件判断和阻塞操作是原子操作,即要保证这两者之间没有其他线程插入干扰。条件变量通常与互斥锁(mutex
)结合使用,以确保线程在 wait
时不会错过条件变化,也不会出现竞争条件。
四、如何解决这些问题
-
加锁机制:你需要使用同一把互斥锁(
mutex
)来保护共享资源的访问,确保wait
和signal
操作的顺序不会乱。wait
之前加锁:等待条件的线程应该在调用wait
之前先加锁,并在wait
内释放锁,但同时阻塞等待信号。signal
时加锁:通知条件满足的线程(通过signal
或notify
)在发出信号时,也应该持有相同的锁,这样就能确保wait
和signal
的正确同步。
-
条件判断:
wait
通常需要在循环中使用,确保条件在每次被唤醒时都重新检查。
五、示例代码(C++)
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
// 等待条件满足
while (!ready) {
cv.wait(lock); // wait时会释放锁,但重新被唤醒时会自动重新获取锁
}
std::cout << "Thread is working!\n";
}
void notifier() {
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
std::unique_lock<std::mutex> lock(mtx); // 获取锁
ready = true; // 改变条件
cv.notify_one(); // 唤醒等待线程
}
int main() {
std::thread t1(worker); // 启动等待线程
std::thread t2(notifier); // 启动通知线程
t1.join();
t2.join();
return 0;
}
解释
worker
线程会等待ready
变量变为true
,通过调用cv.wait(lock)
来释放锁并等待通知。如果ready
还没准备好,线程会阻塞。notifier
线程会在 1 秒后改变ready
的值并通过notify_one()
通知等待中的线程。- 因为
wait
操作在worker
中是与互斥锁结合使用的,所以不会有信号丢失的问题。即使notifier
在worker
还未等待时发送信号,worker
也会正确等待。
注意点
- 锁的作用:锁保护了
ready
这个共享资源,使得worker
和notifier
线程之间的通信是安全的,不会出现竞争条件。 - 条件变量的循环:因为可能存在虚假唤醒(spurious wakeup),即线程被唤醒时条件可能仍然不满足,因此在
wait
返回后应当重新检查条件,这也是为什么常常将wait
放在循环中的原因。