effective c++条款39 使用void futures进行一次性事件通知通讯

有时候某个特定事件的发生时,一个线程需要告知另外一个异步运行的线程,因为后者需要等待这个事件才能执行。这个事件可能指一个数据结构的初始化,某个计算过程完成,或者一个重要的传感器的数据已经被监测到。这些情况下,什么是线程之间最佳的通信方式呢?

最显而易见的是使用条件变量。我们称呼检测条件变量的线程为检测线程。称呼受影响的线程为处理线程。策略很简单:检测线程通知条件变量,而处理线程会等待这个条件变量事件。

std::condition_variable cv;

std::mutex m;

j检测线程内的代码很简单,简单到这样:

...//            检测事件

cv.notify_one();     //  通知反应线程

如果有多个线程需要发送通知,使用notify_all,但现在假定只有一个。

作用线程的代码更复杂些,因为它要先对条件变量调用wait,先通过std::unique_lock 持有互斥量,(先持有锁,然后再等待条件变量,是线程库典型应用。c++11有API,可以使用std::unique_ptr对互斥量加锁。下面是一种模式实现,

{

std::unique_lock<std::mutex> lk(m);  //对m加锁

cv.wait(lk);  //等待notify ,这不对!!

...                  // 处理事件,m在锁定状态

}

                                               //析构函数释放锁

...                                           //继续处理,现在不持有锁了

 

第一个例子有时被称为不好闻的代码味道:即使它有时可以正常工作,但并不完全正确。在这个例子中,气味来自于使用互斥量。

互斥量可以用来控制对共享数据的访问。但检测线程和处理线程不使用这个机制也是完全可能的。举例来说,检测线程初始化一个数据结构对象,然后给处理线程使用。检测线程初始化完对象后,便不在使用此对象。而处理线程在检测线程显示对象准备好了后再进行访问。两个任务通过程序逻辑避免行驶在别人的车道上。这种情况下不需要互斥量,给条件变量加一个锁,这就散发出值得怀疑的设计气味。

即使你忽略上面情况,你仍然需要注意下面两个情况:

检测任务在处理任务等待之前,就发送了条件变量通知。为了唤醒任务,另外的任务必须先等待条件变量。如果检测任务先发送了通知,处理任务就会错过通知,并且永远等待。

等待过程没有考虑到虚假唤醒的情况。多线程的API(许多语言,不光c++)存在这样一个事实,等待条件变量的线程有可能在没有条件变量通知情况下被唤醒。这称为虚假唤醒。需要有合适的代码来确认等待的条件是否真的满足,这通常是唤醒后第一个动作。c++关于条件变量API使这个检测异乎寻常简单的,它接受一个lamda(或者其他函数对象)来进行等待条件的判断。可以这样来些:

cv.wait(lk, []{事件是否发生;});

为了利用这个优势,处理线程需要判断等待条件为真。但我们考虑的场景是检测线程识别事件。处理线程可能无法判断事件是否发生。毕竟它等待条件变量就是为了这。

有很多用条件变量处理线程通信的应用很好的例子,但这个不是。

对于许多开发者来说,第二条锦囊妙计是使用共享的布尔变量。变量初始化为false,当检测线程检测到事件时,设置true。

std::atomic<bool>  flag(false);      // 到条款40 atomic

...           //检测事件

flag=true;   //告诉处理线程

处理线程只是简单的轮询变量值。但检测到值改变,便认为事件发生了。

while(!flag)       //轮询

这个方案避免了条件变量的诸多缺点。不用互斥量,检测线程提早改变变量也没有问题,也没有虚假唤醒问题。很好,很好。

处理线程的轮询不那么好了。在任务轮询标记变量的时候,任务仍然在执行,但几乎是阻塞状态。它占有了本来可以为别的任务服务的硬件线程;它在时间开始和结束的时候,带了上下文切换开销。它在核心可以关闭以节省电力时候,仍然保持着核心的运行。真正阻塞的任务不会有上述问题,这就是条件变量的优势,等待条件变量的任务是真正阻塞的。

混合使用条件变量和标记变量的方法也很常见。标记变量表示事件是否发生,而访问标记变量时需要互斥量来同步。因为互斥量已经阻止了对标记变量的并发访问,所以根据条款40不需要再使用std::atomic类型,简单的boo就满足要求。检测任务看起来这样:

std::condition_variable cv;   //同上

std::mutex m;

bool flag(false);        // 不是原子类型

{

   std::unique_lock<std::mutex> lk(m);  // 加锁

   flag = true;             //告诉处理任务(第一部分)

   }                            //解锁

   cv.notify_one();       //告诉处理任务(第二部分)

下面是处理线程:

...         //准备

{

  std::unique_lock<std::mutex> lk(mutex);

   cv.wait(lk, []{return flag;});  //使用lamda避免虚假唤醒

   ...                     //处理事件,此时持有锁

}

  ...         //继续处理事件,锁已经释放

这个方案避免了我们上面谈到的问题。检测任务在处理线程等待条件变量之前发送通知,可以正常工作。有虚假唤醒时候也可以正常工作。而且也不需要轮询。但还有一点看起来不很好,检测线程必须发送通知去唤醒处理任务,而处理线程唤醒后要先判断下是否真的有通知到来。设置标记变量已经表明事件确定发生了,但检测任务仍然必须发送通知,以便唤醒处理线程检测标记。这个方案虽然可以工作,但是还不够清晰。

另外一种思路是不使用条件变量、互斥量和标记变量,使用future作为检测任务和处理任务之间的通信工具。

std::promise<void> p;

void detect() {

   std::thread t([]{

   p.get_future().wait();  //阻塞到被唤醒

  react();

});

// detect

p.set_value();      //唤醒t线程

t.join();

}

很完美吗? 也不是,在future和promise之间的共享状态变量使用了是动态分配的,所以必须承担基于堆的分配和销毁开销。

而且最大的问题是std::promise只能设置一次。所以你应该只用在单次事件通知上。

注意事项:

简单的条件变量需要一个互斥量,这在检测任务和处理任务之间加入了限制,并且需要处理任务判断事件是否发生。

依靠标记变量的设计避免了上面问题,但设计基于轮询而不是阻塞

将条件变量和标记变量结合起来可以解决问题,但实现起来有点生硬

使用std::future 以及std::promise可以解决问题,但使用了堆上内存,而且限制在一次性通讯。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值