C/C++并发编程—— 并发/并行、多线程内存模型

C++最基础的线程与锁模型资料丰富、简单易学

该模型导致的死锁、饥饿等等问题也是大家很头痛的事情

对于C/C++并发模型,还有很多其它的选择,比如Actor、CSP、协程等

并发编程的基础知识,并发与并行的区别和C/C++多线程内存模型

并发与并行的区别

网络上有很多关于“并发”与“并行”的解释,大家比较认同的是Rob Pike的解释

并发关乎结构,并行关乎执行

并发提供了一种方式让我们能够设计一种方案将问题(非必须的)并行的解决

可以简单理解为“并行 = 并发执行”

不管是多线程程序、多进程程序,在设计和实现阶段应该称之为“并发”,而运行时应该称之为“并行”

可以类比“程序 vs. 进程”,运行时的程序称之为进程。它们都是对同一个事物处在不同阶段/状态时的定义

C/C++多线程内存模型

内存模型和内存布局不是一回事

Linux ELF可执行文件格式,堆、栈、.data段、.text段等

ELF这样的内存布局格式是Linux操作系统对可执行程序的规范

内存模型是编程语言和计算机系统(包括编译器,多核CPU等可能对程序进行乱序优化的软硬件)之间的契约,它规定了多个线程访问同一个内存位置时的语义,以及某个线程对内存位置的更新何时能被其它线程看见

C11/C++11标准之前,C/C++语言没有内存模型的定义

我们天真的认为程序是按顺序一致性(Sequential consistency)模型去运行的,而实际上编译器和多核CPU却是不满足顺序一致性模型的

顺序一致性模型需要满足的两个条件:

  1. 单个线程内指令的执行顺序和代码的顺序是一致的
  2. 多线程的指令执行顺序从全局来看是“串行”执行的

现代CPU的缓存、流水线和乱序执行机制以及编译器的代码优化、重排都无法满足顺序一致性模型

所以,机器实际执行的代码并不是你写的代码

为了在性能和易编程性之间找到平衡,C++11提出了“sequential consistency for data race free programs”内存模型

即没有数据竞争(data race)的程序符合顺序一致性

数据竞争指多个线程在没有同步的情况下去访问相同的内存位置

所以,在C11/C++11后,我们只要对多线程之间需要同步的变量和操作,使用正确的同步原语进行同步,就能保证程序的执行符合顺序一致性

编译器、多核CPU能保证其优化措施不会破坏顺序一致性

理论有些晦涩,例子

1
2
3
4
x = y = 0;
Thread1   	 Thread2
x = 1;     			y = 1;
r1 = y;  		  r2 = x;

按照顺序一致性模型,会有以下5种可能的执行顺序

从分析来看是不会出现“r1 = 0,r2 = 0”的情况的

但是C11/C++11之前并未规定多线程内存模型,也没有多线程的标准库

pthread多线程库是按照“单线程执行模型(Single thread execution model)”来实现的

从编译器的角度来看,不存在什么多线程这样的东西,程序就是一个代码序列

只要编译优化措施不影响顺序执行的结果,就可以执行这项优化。比如下面这种优化

1
2
3
4
5
6
7
Thread1    Thread2
r1 = y;
        			  y = 1;
         		 	 r2 = x;
x = 1;

r1 = 0,r2 = 0

Thread1内的“r1 = y”被换到了“x = 1”之前,这在C11/C++11标准之前是可能发生的

因为按单线程执行模型,“给x赋值1”与“读取y赋值给r1”是两个不相关的事情,调换执行顺序不影响最终结果

而对于C11/C++11标准来说,因为这段代码不存在数据竞争,只要使用标准库提供的线程操作来实现,其执行就符合顺序一致性,不会优化出现这种情况

另外,C11/C++11标准还明确了“内存位置”的定义

一个内存位置要么是标量,要么是一组紧邻的具有非零长度的位域

两个线程可以互不干扰地对不同的内存位置进行读写操作

比如有如下的结构体:

1
2
3
4
struct {
	int a : 17;
	int b : 15;
} x;

两个线程分别读写a和b,是否会互相干扰呢?

毕竟CPU是按32/64位来取操作数的,而不是按17/15位来的

C11/C++11之前这样的操作是未定义的,按C11/C++标准规定a和b则属于同一个内存位置

两个线程分别对a、b进行读写操作是会相互干扰的,需要进行同步。或者将a、b分割成两个内存位置

1
2
3
4
5
struct {
	int a : 17;    // 内存位置1
	int : 0;
	int b : 15;    // 内存位置2
} x;

这样编译器会自动自行内存对齐,保证两个线程分别读写a、b互不干扰

https://www.jianshu.com/p/298296e9a887