《Effective Modern C++》学习笔记 - Item 39: 考虑使用void future实现单次事件通信

本文探讨了线程间事件通信的几种方法,包括使用condition_variable、原子标志(atomic flag)以及结合两者的方式。指出每种方法的优缺点,如虚假唤醒、忙等待和资源管理。最后提出使用std::promise和std::future实现线程挂起与唤醒,作为简洁高效的解决方案。此方法允许主线程设置future,副线程等待future完成,从而实现线程的同步。
摘要由CSDN通过智能技术生成
  • 本节是一个小技巧,告诉我们可以使用 void 型的 future 实现线程间一次性的事件通信。

  • 如何实现两个线程间事件的通信?(例如,副线程需要等待一个数据才能继续计算,主线程将该数据准备完成后应通知副线程继续运行。以下代码中原书分别将其称为 react 和 detect 线程。)

  • 一种方法是使用 std::condition_variable:创建变量 cv,副线程调用 wait 阻塞等待信号;主线程准备完成后调用 notify_onenotify_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::futureshare() 函数将结果的控制权移交给一个可以共享的 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 threadsfor (auto& t : vt) { 	// make all threads
		t.join(); 			// unjoinable; see Item 2
	} 						// for info on "auto&"
}

总结

  1. 对于简单的事件通讯,基于 condvar 的设计需要一个多余的 mutex,对于发出和等待信号的顺序有限制,而且还需要等待线程检验事件是否真正发生。
  2. 使用 flag 的设计避免了这些问题,但是其本质是轮询而非阻塞。
  3. condvar 和 flag 可以被结合使用,但结果得出的通讯机制有些不自然。
  4. 使用 std::promise 和 future 避免了这些问题,但注意其使用的 shared states 基于堆内存,而且仅限于一次性的通讯。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值