有时候我们编写并发程序可能会遇到困难,尽管程序编译运行正确,但编写起来或许有些困惑,这是最佳实践么?所以本文介绍相关C++核心准则来指导你编写正确的并发程序。
并发的核心编程主要有如下原则:
1.避免数据竞争;2.尽可能避免写共享数据;3.从任务的角度来考虑而不是线程;4.不要用volatile同步;1.避免数据竞争
数据竞争是指同时写入和读取数据,这样可能会导致冲突。为了避免数据竞争,建议你使用静态变量这种十分经典的解决方案,具体如下(--正面案例):
intget_id(){staticintid=1;returnid++;}单单从上述代码,还看不出有什么问题。假设线程A和线程B读取id相同的值k,那线程A和线程B将值k+1写回,所以最后id值k+1就会存在两次,但不会发生冲突。
这里再举个switch和case代码块案例(--反面案例):
unsignedval;if(val5){switch(val){case0://...case1://...case2://...case3://...case4://...}}编译器通常会将switch块实现为跳转表,为了理解简单,你可以等价于如下代码。
if(val5){//(1)functions[val]();}如果val等于3,functions[3]()代表切换块的功能。假设现在存在这个可能发生,另一个线程进入并更改(1)处的值,使其不在合理的范围内,这时就会发生不确定的行为。
2.尽可能避免写共享数据
内存共享模型
这是很容易遵循但很重要的规则。如果你要共享数据,则应尽可能避免写入共享变量。因此,你现在只需要考虑如何用线程安全的方式初始化共享数据。在C++11中推荐如下几种实现方式:
(1).在启动线程之前进行数据初始化,这种方式很好理解,也采用的最多。
constintval=;threadt1([val]{....};threadt2([val]{....};(2).使用常量表达式,因为它们在编译时会初始化。
constexprautodoub=5.1;(3).将功能std::call_once与std::once_flag结合起来使用。你可以将重要的初始化内容放入onlyOnceFunc函数中,C++在运行时保证此函数只运行一次。
std::once_flagonceFlag;voiddo_once(){std::call_once(onceFlag,[](){std::coutImportantinitialisationstd::endl;});}std::threadt1(do_once);std::threadt2(do_once);std::threadt3(do_once);std::threadt4(do_once);(4).将静态变量与函数块一起使用,因为在C++11运行时会保证以线程安全的方式对其进行初始化。
voidfunc(){....staticintval;....}threadt5{func()};threadt6{func()};3.从任务的角度来考虑而不是线程
tasks内存模型
什么是任务呢?tasks是C++11中的一个术语,是额外添加的C++标准,这是给了一个比线程更好的抽象。tasks内存模型由发送者通常被称为promise和接收者被称为future组成。分别用线程和tasks内存模型,计算1+1的总和,示例如下:
//threadintres;threadt([]{res=1+1;});t.join();coutresendl;//taskautofut=async([]{return1+1;});coutfut.get()endl;Promise和Future跟线程之间的根本区别是什么?线程是