Item 39: Consider void futures for one-shot event communication.

Item 39: Consider void futures for one-shot event communication.

对于两个异步任务,经常需要一个任务(检测线程)告诉另一个任务(反应线程)特定的事件已经发生了,反应线程可以继续执行了。这个事件可能是某个数据结构被初始化了,某一阶段计算完成了,或者一个传感器数据已经采集好了。需要一种机制来完成两个任务线程间的通信,有哪些比较好的方法呢?

使用条件变量

一个明显的方法就是使用条件变量。检测线程在特定事件发生后,通过条件变量通知反应线程。反应线程需要借助 std::mutexstd::unique_lockstd::unique_lockstd::lock_guard 都是管理锁的工具,都是 RAII 类;它们都是在定义时获得锁,在析构时释放锁。它们的主要区别在于 std::unique_lock 管理锁机制更加灵活,可以再需要的时候进行 lock 或者 unlock ,不必须得是析构或者构造时。因而为了防止线程一直占用锁,条件变量选择和 std::unique_lock 一起工作,条件变量的 wait 系列方法会在阻塞时候自动释放锁)。代码逻辑如下:

std::condition_variable cv;   // condvar for event
std::mutex m;                 // mutex for use with cv

// 检测线程// detect event
cv.notify_one();      // tell reacting task
// cv.notify_all();   // tell multiple reacting task

// 反应线程// prepare to react
{                                        // open critical section
  std::unique_lock<std::mutex> lk(m);    // lock mutex
  cv.wait(lk);                           // wait for notify;
                                         // this isn't correct!// react to event
                                         // (m is locked)
}                                        // close crit. section;
                                         // unlock m via lk's dtor// continue reacting
                                         // (m now unlocked)

上述代码除了使用锁使程序变得复杂以外,还存在以下问题:

  • 如果检测线程在反应线程 cv.wait 前发出通知,反应线程将会错过通知而永远不会被唤醒。
  • 反应线程的 cv.wait 存在被虚假唤醒的可能(由于操作系统的问题,wait 在不满足条件时,也可能被唤醒,也即虚假唤醒)。虽然可以给 wait 传谓词参数,用于判断是否为真的唤醒,但是多数情况先并没有好的判断方法。
cv.wait(lk,
        []{ return whether the event has occurred; });

使用共享的flag

大家可能会想到使用一个共享的 flag 来实现不同线程的同步。代码逻辑如下:

std::atomic<bool> flag(false);   // shared flag; see Item 40 for std::atomic

// 检测线程 // detect event
flag = true;         // tell reacting task

// 反应线程// prepare to react
while (!flag);      // wait for event// react to event

这种方法的缺点是反应线程在等待过程中不阻塞的,而是轮询机制,一直处在运行状态,也就是仍然占用硬件资源。

使用条件变量加共享的flag

还可以将条件变量和共享 flag 结合使用,flag 表示是否为发生了关心的事件。通过 std::mutex 同步访问 flag,就无需使用 std::atomic 类型的 flag 了,只要简单的 bool 类型即可。

std::condition_variable cv;
std::mutex m;
bool flag(false);

// 检测线程  // detect event
{
  std::lock_guard<std::mutex> g(m);  // lock m via g's ctor
  flag = true;                       // tell reacting task
                                     // (part 1)
}                                    // unlock m via g's dtor
cv.notify_one();                     // tell reacting task
                                     // (part 2)

// 反应线程// prepare to react
{ 
  std::unique_lock<std::mutex> lk(m);
  cv.wait(lk, [] { return flag; });    // use lambda to avoid
                                       // spurious wakeups// react to event
                                       // (m is locked)
}// continue reacting
                                       // (m now unlocked)

这种方法功能上没有什么问题,就是代码稍微复杂了些。

使用 future

Item38 中介绍了 std::futurestd::promise 的通信方式。std::future 内部存储了一个将来会被赋值的值,并可以通过 get 方法访问。而 std::promise 在将来给这个值赋值,每个 std::promise 内部都有一个 std::future 对象,std::promise 和其内部的 std::future 共享这个值。我们并不关心这个值具体是啥,因而 std::promisestd::future 的模板类型使用 void 即可。代码逻辑如下:

std::promise<void> p;      // promise for communications channel

// 检测线程// detect event
p.set_value();     // tell reacting task

// 反应线程// prepare to react
p.get_future().wait();  // wait on future corresponding to p// react to event

使用这种方法的优点包括:避免了使用 mutexwait 是真阻塞的,也没有条件变量的 notifywait 之前执行的问题。

当然这种方法也有缺点。首先 std::futurestd::promise 间的共享状态是动态申请的堆内存,需要堆资源的申请和释放,有一定的开销。更重要的问题是,由于 std::promise 只能设置值一次,因而这种通知机制是一次性的。

假设你想让反应线程创建后暂停执行,直到期望的事件发生后继续执行,使用基于 future 的方法是一个不错的选择。例如:

std::promise<void> p;
void react();           // func for reacting task

void detect()                             // func for detecting task
{
  std::thread t([]                        // create thread
                {
                  p.get_future().wait();  // suspend t until
                  react();                // future is set
                });// here, t is suspended
                                          // prior to call to react
  p.set_value();                          // unsuspend t (and thus
                                          // call react)// do additional work
  t.join();                               // make t unjoinable
}                                         // (see Item 37)

为了让 detect 的所有出口 t 都是 unjoinable 的,应该使用 Item37 中介绍的 ThreadRAII 类的,例如:

void detect()
{
  ThreadRAII tr( // use RAII object
    std::thread([]
                {
                  p.get_future().wait();
                  react();
                }),
    ThreadRAII::DtorAction::join // risky! (see below)
  );// thread inside tr
                 // is suspended here
  p.set_value(); // unsuspend thread
                 // inside tr}

然而,上述代码还存在问题。如果在第一个 “…” 的地方发生异常,pset_value 不会被执行,那么 lambda 函数中的 wait 永远不会返回,由于 tr 的类型是 join 的,则 tr 的析构永远不会完成,代码将会挂起(见 http://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html 中的相关讨论)。

这里给出不使用 RAII 类 Thread 的方法使其挂起然后取消挂起,这里关键是使用 std::shared_future 代替 std::futurestd::futureshare 成员函数将共享状态所有权转移到 std::shared_future

std::promise<void> p;
void detect()                          // now for multiple
{                                      // reacting tasks
  auto sf = p.get_future().share();    // sf's type is
                                       // std::shared_future<void>
  std::vector<std::thread> vt;         // container for
                                       // reacting threads
  for (int i = 0; i < threadsToRun; ++i) {
    vt.emplace_back([sf]{ sf.wait();   // wait on local
                          react(); }); // copy of sf; see
  }                                    // Item 42 for info
                                       // on emplace_back// detect hangs if
                                       // this "…" code throws!
  p.set_value();                       // unsuspend all threadsfor (auto& t : vt) {                 // make all threads
    t.join();                          // unjoinable; see Item 2
  }                                    // for info on "auto&"
}

这样,就可以很好地使用 future 实现线程间的一次性通信。

参考:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值