[modern c++] 条件变量的使用
原创
©著作权归作者所有:来自51CTO博客作者obentul的原创作品,请联系作者获取转载授权,否则将追究法律责任
机制:
条件变量需要一个 mutex-like 对象与之配合使用,发送信号的一方在获取锁后便可以随时发送信号,等待信号一方需要通过调用 wait 阻塞等待锁,当发送方的notify_one被调用时,其内部会进行解锁动作,这会导致wait跳出阻塞(在wait内部跳出加锁动作的阻塞,不是wait函数跳出阻塞),紧接着立刻校验注册给 wait 的 条件(一个callable对象),如果条件为真 则先对刚才的锁再次加锁, 然后执行wait的下一条代码(这里才是wait阻塞的跳出点),否则不对刚才的锁加锁并继续等待(继续阻塞在wait上)。当条件为真时,我们需要记住此时此刻我们是持有锁的,因此发送信号的一方会被阻塞在获取锁的位置。
Tips:上一段话中的斜体且有下划线的部分表示这些代码是在wait内部进行的,这也是条件变量的内部实现机制,Windows下如果想通过Event来实现条件变量,只需要按照这些原则实现即可。
注意:
- 条件变量的信号不会排队,如果发送信号的一方在发送信号时还没有任何线程通过 wait 等待信号,那么这次信号会被丢弃(类比Windows下Event的脉冲模式,与之相反的是Windows下Event的电平模式)。
- wait 跳出阻塞是并不 100% 保证条件已经满足,我们还需要二次确认。因为有时候会出现spurious wakeup。
- wait 有其他两个版本,一个是 wait_for,一个是 wait_until ,二者都是用来附加一个超时机制,当超时时间到达则会跳出 wait 阻塞,这个时候我们需要人为判断是不是条件得到满足了。
- 当wait条件满足的时候,wait跳出阻塞向下执行,此时wait所在的那个线程是持有锁的,发送信号线程如果再次对锁加锁将被阻塞住。
- notify_one不会阻塞,发送信号的线程只会在试图加锁时被阻塞。
代码片段:
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
void data_preparation_thread()
{
while (more_data_to_prepare())
{
data_chunk const data = prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one();
}
}
void data_processing_thread()
{
//下面两种场景是等同的,这里使用unique_lock的目的是让data_preparation_thread不在mut的获取上长时间阻塞,因为process(data);语句可能会非常耗时间
//如果不想用unique_lock,可以使用 {} 增加一个作用域来 人为触发 lock_guard 的析构,从而在 process(data); 之前释放 mut
#if 1
while (true)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [] {return !data_queue.empty(); });
data_chunk data = data_queue.front();
data_queue.pop();
lk.unlock();
process(data);
if (is_last_chunk(data))
break;
}
#else
while (true)
{
{
std::lock_guard<std::mutex> lk(mut);
data_cond.wait(lk, [] {return !data_queue.empty(); });
data_chunk data = data_queue.front();
data_queue.pop();
}
process(data);
if (is_last_chunk(data))
break;
}
#endif
}
使用条件变量的线程安全队列:
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut;
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{}
threadsafe_queue(threadsafe_queue const& other)
{
std::lock_guard<std::mutex> lk(other.mut);
data_queue = other.data_queue;
}
void push(T new_value)
{
std::lock_guard<std::mutex> lk(mut);
data_queue.push(new_value);
data_cond.notify_one();
}
void wait_and_pop(T& value)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {return !data_queue.empty(); });
value = data_queue.front();
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop()
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] {return !data_queue.empty(); });
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool try_pop(T& value)
{
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return false;
value = data_queue.front();
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool empty() const
{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};
模拟WINDOWS的Event:
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <memory>
#include <thread>
#include <Windows.h>
std::mutex m;
std::condition_variable cv;
void func1() {
while (1) {
std::unique_lock<std::mutex> l(m);
cv.wait(l);
std::cout << "func1"<<std::endl;
}
}
void func2() {
while (1) {
std::unique_lock<std::mutex> l(m);
cv.wait(l);
std::cout << "func2" << std::endl;
}
}
void func3() {
while (1) {
std::unique_lock<std::mutex> l(m);
cv.wait(l);
std::cout << "func3" << std::endl;
}
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
std::thread t3(func3);
while (1) {
std::unique_lock<std::mutex> l(m);
cv.notify_all();
l.unlock();
Sleep(1000);
std::cout << "================\n";
}
std::cout << "Hello World!\n";
while (1);
}