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却是不满足顺序一致性模型的
顺序一致性模型需要满足的两个条件:
- 单个线程内指令的执行顺序和代码的顺序是一致的
- 多线程的指令执行顺序从全局来看是“串行”执行的
现代CPU的缓存、流水线和乱序执行机制以及编译器的代码优化、重排都无法满足顺序一致性模型
所以,机器实际执行的代码并不是你写的代码
为了在性能和易编程性之间找到平衡,C++11提出了“sequential consistency for data race free programs”内存模型
即没有数据竞争(data race)的程序符合顺序一致性
数据竞争指多个线程在没有同步的情况下去访问相同的内存位置
所以,在C11/C++11后,我们只要对多线程之间需要同步的变量和操作,使用正确的同步原语进行同步,就能保证程序的执行符合顺序一致性
编译器、多核CPU能保证其优化措施不会破坏顺序一致性
理论有些晦涩,例子
|
|
按照顺序一致性模型,会有以下5种可能的执行顺序
从分析来看是不会出现“r1 = 0,r2 = 0”的情况的
但是C11/C++11之前并未规定多线程内存模型,也没有多线程的标准库
pthread多线程库是按照“单线程执行模型(Single thread execution model)”来实现的
从编译器的角度来看,不存在什么多线程这样的东西,程序就是一个代码序列
只要编译优化措施不影响顺序执行的结果,就可以执行这项优化。比如下面这种优化
|
|
Thread1内的“r1 = y”被换到了“x = 1”之前,这在C11/C++11标准之前是可能发生的
因为按单线程执行模型,“给x赋值1”与“读取y赋值给r1”是两个不相关的事情,调换执行顺序不影响最终结果
而对于C11/C++11标准来说,因为这段代码不存在数据竞争,只要使用标准库提供的线程操作来实现,其执行就符合顺序一致性,不会优化出现这种情况
另外,C11/C++11标准还明确了“内存位置”的定义
一个内存位置要么是标量,要么是一组紧邻的具有非零长度的位域
两个线程可以互不干扰地对不同的内存位置进行读写操作
比如有如下的结构体:
|
|
两个线程分别读写a和b,是否会互相干扰呢?
毕竟CPU是按32/64位来取操作数的,而不是按17/15位来的
C11/C++11之前这样的操作是未定义的,按C11/C++标准规定a和b则属于同一个内存位置
两个线程分别对a、b进行读写操作是会相互干扰的,需要进行同步。或者将a、b分割成两个内存位置
|
|
这样编译器会自动自行内存对齐,保证两个线程分别读写a、b互不干扰