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

最近看了《七周七并发模型》[1],对自己熟悉的C/C++并发编程有了很多新的思考。在Google上搜索“C C++ 并发 编程”,结果主要是Anthony的《C++ Concurrency in Action》以及零散的一些博文。Anthony的书主要是教授C++最基础的线程与锁模型和无锁编程的知识,但是其它的并发模型书中并未提及。线程与锁模型因其资料丰富“简单易学”被广大C/C++程序员所使用。该模型导致的死锁、饥饿等等问题也是大家很头痛的事情。实际上对于C/C++并发模型,我们还有很多其它的选择,比如Actor、CSP、协程等,而这正是这个C/++并发编程系列要告诉大家的。开篇先说一下并发编程的基础知识,并发与并行的区别和C/C++多线程内存模型。
并发与并行的区别?
网络上有很多关于“并发”与“并行”的解释,大家比较认同的是Golang大神Rob Pike在“并发不是并行[3]”的技术分享上的解释:

Concurrency vs. parallelism
Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.
Not the same, but related.
Concurrency is about structure, parallelism is about execution.并发关乎结构,并行关乎执行。
Concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.并发提供了一种方式让我们能够设计一种方案将问题(非必须的)并行的解决。[2]

按我个人对以上的理解,“并行”和“并发”的区别,可以简单理解为“并行 = 并发执行”。不管是多线程程序、多进程程序,在设计和实现阶段应该称之为“并发”,而运行时应该称之为“并行”。可以类比我们熟悉的“程序 vs. 进程”,运行时的程序称之为进程。它们都是对同一个事物处在不同阶段/状态时的定义。

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

以前我认为内存模型和内存布局是一回事,比如Linux下ELF可执行文件格式,堆、栈、.data段、.text段等等。实际上ELF这样的内存布局格式是Linux操作系统对可执行程序的规范,不管用什么编程语言生成了直接(依赖运行时“虚拟机”的语言除外)可运行的程序,最终都是ELF的内存布局。而内存模型是编程语言和计算机系统(包括编译器,多核CPU等可能对程序进行乱序优化的软硬件)之间的契约,它规定了多个线程访问同一个内存位置时的语义,以及某个线程对内存位置的更新何时能被其它线程看见[4]
在C11/C++11标准之前,C/C++语言没有内存模型的定义。在此期间,我们天真的认为程序是按顺序一致性(Sequential consistency)模型去运行的,而实际上编译器和多核CPU却是不满足顺序一致性模型的。Leslie在其论文[6]中定义了顺序一致性模型需要满足的两个条件:

Rl: Each processor issues memory requests in the order specified by its program.

R2: Memory requests from all processors issued to an individual memory module are serviced from a single FIFO queue. Issuing a memory request consists of entering the request on this queue.

条件“R1”可以理解为“单个线程内指令的执行顺序和代码的顺序是一致的”,而条件“R2”则让多线程的指令执行顺序从全局来看是“串行”执行的。现代CPU的缓存、流水线和乱序执行机制以及编译器的代码优化、重排都无法满足顺序一致性模型。所以,机器实际执行的代码并不是你写的代码[9]

为了在性能和易编程性之间找到平衡,C++11提出了“sequential consistency for data race free programs”内存模型,即没有数据竞跑(data race)的程序符合顺序一致性。数据竞跑是指多个线程在没有同步的情况下去访问相同的内存位置[5]。所以,在C11/C++11后,我们只要对多线程之间需要同步的变量和操作,使用正确的同步原语进行同步,就能保证程序的执行符合顺序一致性。编译器、多核CPU能保证其优化措施不会破坏顺序一致性。
理论有些晦涩,我引用个例子说明。如下:

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)”来实现的。从编译器的角度来看,不存在什么多线程这样的东西,程序就是一个代码序列。只要编译优化措施不影响顺序执行的结果,就可以执行这项优化。比如下面这种优化:

6
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标准来说,因为这段代码不存在数据竞跑,只要使用标准库提供的线程操作来实现,其执行就符合顺序一致性,不会优化出现“6”这种情况。

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

一个内存位置要么是标量,要么是一组紧邻的具有非零长度的位域。
两个线程可以互不干扰地对不同的内存位置进行读写操作

比如有如下的结构体:

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分割成两个内存位置:

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

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

参考
[1]《七周七并发模型》,Paul Butcher 著,黄炎 译
[2] 也谈并发与并行,Tony Bai
[3] Concurrency is not parallelism,Rob Pike
[4] 浅析C++多线程内存模型,Guancheng (G.C.)
[5] Race Condition vs. Data Race,John Regehr
[6] How to Make a Multiprocessor Computer That Correctly Executes Multiprocess Programs,1979,Leslie Lamport
[7] 《C++0x漫谈》系列之:多线程内存模型,刘未鹏
[8] ISO/IEC JTC1 SC22 WG21 N3690,Programming Languages — C++
[9] C++ Memory Model,Valentin .etc

修订记录
2017-11-01 AM:从网易博客迁移到简书
2017-09-30 PM:完成初稿

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容