-
本节是一个小技巧,告诉我们可以使用
void
型的 future 实现线程间一次性的事件通信。 -
如何实现两个线程间事件的通信?(例如,副线程需要等待一个数据才能继续计算,主线程将该数据准备完成后应通知副线程继续运行。以下代码中原书分别将其称为 react 和 detect 线程。)
-
一种方法是使用
std::condition_variable
:创建变量cv
,副线程调用wait
阻塞等待信号;主线程准备完成后调用notify_one
或notify_all
发出信号,简单代码如下:
std::condition_variable cv; // condvar for event
std::mutex m; // mutex for use with cv
// 主线程
{
… // detect event
cv.notify_one(); // tell 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)
- 以上代码从感觉上就不太对劲(原文用了一个有趣的描述:code smell,直译即这段代码 “有味” ww),而且它并不正确,具体来说有以下问题:
- 如果主线程 早于 副线程调用
wait
时就已经调用notify_one
通知了 condvar,那么副线程将永远阻塞下去。 wait
调用存在 虚假唤醒(spurious wakeups) 现象,即 condvar 没有被通知时,等待的代码也有可能被唤醒。这是底层实现导致的,很多编程语言的多线程 API 都存在这个问题。虽然 C++ 的wait
函数有一个接受一个额外的函数参数,用于检查事件是否真正发生的重载版本,但在本节的情景中,我们无法做出判断,因为我们等待的是一个一次性的事件。
- 如果主线程 早于 副线程调用
- 另一种简单粗暴的方法是直接用一个布尔值的 flag。由于多线程情景,我们应该对其用
std::atomic
包装:
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
这种方法不存在以上的两个问题,但其自身问题更明显:该线程在被通知之前并没有真正被阻塞,而是在 忙等待,或者说 轮询(polling),不断循环探测信号,这对性能有较大的消耗。
- 以上两种设计可以被结合起来,得到一种避免了所有问题的方案:
std::condition_variable cv; // as before
std::mutex m;
// 主线程
bool flag(false); // not std::atomic
… // 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
{ // as before
std::unique_lock<std::mutex> lk(m); // as before
cv.wait(lk, [] { return flag; }); // use lambda to avoid spurious wakeups
… // react to event (m is locked)
}
… // continue reacting (m now unlocked)
//
虽然这段代码从功能上没有问题,但 “味更冲了”。
- 一种避免使用 condvar,mutex 和 flag 的替代方案是使副线程等待一个主线程设置的 future。Item 38 已经解释了
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
利用这种方法可以实现一个实用但C++标准没有提供的功能:创建一个线程后先将其 挂起(suspend),满足一定条件后再执行,代码如下:
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)
}
如果需要同时控制多个线程,利用 std::future
的 share()
函数将结果的控制权移交给一个可以共享的 std::shared_future
即可:
std::promise<void> p; // as before
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 copy of sf;
react(); }); // 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&"
}
总结
- 对于简单的事件通讯,基于 condvar 的设计需要一个多余的 mutex,对于发出和等待信号的顺序有限制,而且还需要等待线程检验事件是否真正发生。
- 使用 flag 的设计避免了这些问题,但是其本质是轮询而非阻塞。
- condvar 和 flag 可以被结合使用,但结果得出的通讯机制有些不自然。
- 使用
std::promise
和 future 避免了这些问题,但注意其使用的 shared states 基于堆内存,而且仅限于一次性的通讯。