Daily C/C++ condition_variable
之前有说要出一个条件变量的讲解
说到头我还是感觉cppreference讲的好,这里就总结一下
条件变量也是一种用于同步的对象,他能够阻塞多个线程,直到另一个线程修改了共享变量并通知条件变量
这是啥意思呢,我们先来一个例子
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
// 等待直至 main() 发送数据
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});
// 等待后,我们占有锁。
std::cout << "Worker thread is processing data\n";
data += " after processing";
// 发送数据回 main()
processed = true;
std::cout << "Worker thread signals data processing completed\n";
// 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
lk.unlock();
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
// 发送数据到 worker 线程
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();
// 等候 worker
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
首先我们会创建一个worker线程,然后持有锁。
接着我们会使用cv.wait()
进入等待状态,这时候worker线程就会释放掉这个锁,同时等待谓词返回真,这里就是等待ready为真
然后来到main中,他会首先获得锁,然后修改ready,并之后通知条件变量唤醒线程
接着worker被唤醒以后,首先他会尝试去获得锁,然后检查谓词,如果满足,他就会退出等待,并执行下一步的代码
与此同时,main中也会尝试去获得锁,并等待worker修改processed
worker中被唤醒后就会一直持有锁,他会修改processed,然后再是释放掉锁,最后通知条件变量去唤醒主线程
看完这个例子,再看一下条件变量的使用方法就会清楚很多
有意要修改共享变量的线程必须要获得锁(因为是共享变量)
然后再修改共享变量,最后执行notify_one
或者notify_all
来唤醒其他等待的线程。
即使共享变量是原子的,我们也必须要在互斥的状态下修改他,以保证共享变量被正确的发布(个人猜测可能和高速缓存一致性有关,因为锁可以帮我们来统一高速缓存)
有意在条件变量上等待的线程必须获得std::unique_lock<std::mutex>
,因为我们是在通过这个锁进行同步
然后我们或者会去检查条件,或者执行等待。
当条件变量被通知时,线程将被唤醒,同时他会尝试去获得互斥(因为要先有锁才能去读共享变量),然后去检查条件
具体的细节上的使用方法大家可以参阅cppreference
这里就说两个,notify和wait,也是最主要的两个
notify就是在条件变量上解阻塞一个或若干个线程,让他们进行检查
wait则会原子的释放lock,并阻塞当前的线程。我们可以通过传入一个谓词来让被唤醒的线程检查条件
等价于
while (!pred()) {
wait(lock);
}
注意,通知线程的时候不必保有互斥锁。实际上这样做是劣化,因为被通知的线程将立即再次阻塞,等待通知线程释放锁。
有些实现会辨识此情形,在通知调用中,他会从条件变量队列转移等待线程到互斥队列,而不唤醒他。
文章评论