Item 39: Consider void futures for one-shot event communication.
对于两个异步任务,经常需要一个任务(检测线程)告诉另一个任务(反应线程)特定的事件已经发生了,反应线程可以继续执行了。这个事件可能是某个数据结构被初始化了,某一阶段计算完成了,或者一个传感器数据已经采集好了。需要一种机制来完成两个任务线程间的通信,有哪些比较好的方法呢?
使用条件变量
一个明显的方法就是使用条件变量。检测线程在特定事件发生后,通过条件变量通知反应线程。反应线程需要借助 std::mutex
和 std::unique_lock
(std::unique_lock
和 std::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::future
和 std::promise
的通信方式。std::future
内部存储了一个将来会被赋值的值,并可以通过 get
方法访问。而 std::promise
在将来给这个值赋值,每个 std::promise
内部都有一个 std::future
对象,std::promise
和其内部的 std::future
共享这个值。我们并不关心这个值具体是啥,因而 std::promise
和 std::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
使用这种方法的优点包括:避免了使用 mutex
,wait
是真阻塞的,也没有条件变量的 notify
在 wait
之前执行的问题。
当然这种方法也有缺点。首先 std::future
和 std::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
…
}
然而,上述代码还存在问题。如果在第一个 “…” 的地方发生异常,p
的 set_value
不会被执行,那么 lambda 函数中的 wait
永远不会返回,由于 tr
的类型是 join
的,则 tr 的析构永远不会完成,代码将会挂起(见 http://scottmeyers.blogspot.com/2013/12/threadraii-thread-suspension-trouble.html 中的相关讨论)。
这里给出不使用 RAII 类 Thread 的方法使其挂起然后取消挂起,这里关键是使用 std::shared_future
代替 std::future
,std::future
的 share
成员函数将共享状态所有权转移到 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 threads
…
for (auto& t : vt) { // make all threads
t.join(); // unjoinable; see Item 2
} // for info on "auto&"
}
这样,就可以很好地使用 future 实现线程间的一次性通信。
参考: