C++多线程内存模型,memory_order杂谈。

release-acquire是需要配对使用的

为什么要配对使用?
release相当于把当前线程下release之前所有的数据实时更新了以下,但要特别注意的是,是只有调用acquire的那个线程会看到这个更新。
所以他俩当然要一起使用了,你光release,不acquire,其他线程获取的都是乱序的,release等于relaxed了,你光acquire不release,acquire的也是relaxed的。

那你说为什么要这么搞,release直接给全线程更新就得了呗,要啥acquire啊。

理想很美好,我们首先仔细想下,relaxed为什么要存在?我看很多篇文章的意思是relaxed什么都不保证,跟正常存一样。其实这就是它的保证,它保证了速度
什么叫不保证:当你用relaxed存这个数据的时候意味着其他线程如果load这个数据,它load的值是无法确定的,或许是你刚store的,也或许是你之前store的。
这种情况是很常见的,因为是多线程并发,共享的全局变量刚被这个线程读,另一个线程就把它改了,那读的这个线程读到的准定就不是最新的了。这种情况还好说,这属于你人为的造成了脏读。更甚的一种情况是明明我这个线程已经store完了,那个线程load的还就不是我这线程store的值。(原因继续往下读,在很下面)。这当然不好,但是这样多线程并发速度才会快啊,如果想要读到的都是实时更新的数据有两种办法,一是在所有读操作前,都保证已经写了,也就是序列的一致,这就好像在单线程下运行一样,可想而知性能之低,二是得在CPU与主存之间下功夫,也能保证每次读都是读的最新更新数据(见文末猜测),必然也会耗费大量时间。所以有些数据,该更新时得更新,没这必要时就用relaxed存。这就是relaxed的作用。
如果真的需要全线程都知道我这数据更新了,那就用seq。seq是原子变量默认的存储方式,是顺序的。顺序的意思就是,我这用seq存了,ok,你其他线程要load这个数据,load到的一定会是我这存的。当然了,如果还有一个线程在我用seq存了这数据后,它也用seq把这数据改了,那ok,他是大哥了,等着load的就是他存的。
没错,如果所有数据都是这样用seq存,效率可想而知。并且这个seq,就是最开始所想的那个一次release,所有线程更新。

所以release-acquire也就应运而生了。我release是对所有线程relaxed,唯独对acquire的线程seq。效率与有序双丰收,欸美滋滋。

那最终极的问题出现了,这些存取方式到底怎么实现的?
首先是relaxed,这个最好想,它的方式就是没有方式。就正常存就行。
怎么叫正常就行呢,首先大家一定要明确的一点就是,我们所写的代码,执行起来可不是我们写的顺序,甚至不是我们写的东西。
只要不真正的影响语义,编译器和CPU是乱序执行的。不是真正的随便乱着来。他们给咱优化了,比如说int x=1; x=2;
编译器觉得你这x赋值了个1,然后又赋值2,那赋1没得卵用,就给你优化掉了!
人家初衷是好的,而且确实给咱提速了,但是这在多线程的环境下就是个噩梦,我举个最简单的例子,这个线程下本来是x = true; y = false;
编译器,或者CPU,人家正好就是认为反过来速度快(那就确实会快),然后就给你变成了 y = false; x = true;这在单线程下没啥,因为单线程内,这个x,y谁先赋值无所谓,人家也是了解这点的所以才敢给你换,但多线程情况下,每一个线程依旧会认为自己是单线程的,因为线程是CPU调度的最小单位,他们以为他们自己就是整个宇宙。所以这个宇宙是意识不到另一个平行宇宙内正在判断y是不是false,y要是false另一个宇宙就认为x准定是true,这样毛病不就大了么,另一个宇宙可不知道x已经被编译器好心的交换到y的后面了。
这就是乱序。正常存就行的意思就是指,就这么乱序的存,就行。但也还是有区别的,原子变量之所以叫原子变量,就是因为所有对它的操作都得是原子性的。
也就是说,你使用原子变量会保证,这个变量在被这个线程存的时候,其他线程不能存,只能读。
这就很有用了,正常变量可保证不了这点,正常变量是存在被这个线程写了一半时,另一个线程又开始写它的情况的,这不就窜了么,结果不得而知反正是灾难性的。
说完relaxed,再谈seq,seq可是原子类型默认的存储方式。它必然有着默认的道理。
一个程序能够正常的运行下来,必然是顺序执行下去的,乱序也是只单线程在顺序的基础上乱,这是无伤大雅甚至锦上添花的。可在多线程的情况下,这一无伤大雅的乱,可就真的乱了,所以会将seq置为默认。
因为seq能够保证我用seq存的就是顺序的,所有线程读都是顺序的,我seq不会乱。
我发现我也只能说到此了,因为我目前好像还真不能说太清它再底层是咋实现的,Herb Sutter有个专门的演讲,我看过一点:
首先我们先想,线程是CPU调度的最小单位,进程才是资源调度的最小单位,线程之间是共享内存的,而程序在运行时内存分配有3种(我前些天正好还写了篇博客):栈,堆,静态存储区。
而那些线程内的局部变量明显是存在了栈,你自己手工创建的(new/malloc)存在了堆,这些都不用管,需要管的是存在了静态存储区的全局变量。我们存的都是这些存在静态存储区的全局变量。
我们存一个变量时(以下称数据),其实不会直接就更新内存,这里的内存指的是主存。相信大家都知道或者听说过cache吧。CPU的运算速度是巨快的,如果CPU直接存储给主存,着实大材小用,因为需要写数据给主存,I/O时间会显的很长,这时cache(高速缓存)就登场了,CPU快速的把数据写入cache,然后它就可以继续做其他事了。cache也分级别的,L1,L2,L3,层层递进,最后存到主存。级别不是关键,关键是我们要知道CPU不是直接和内存沟通的,其实这就是乱序的根源!(从此以下的言论,或不属实,还望大佬指点)
要知道主存内的那3个区可才是真正存数据的地方啊。你CPU等于说为了提高效率,还没真正存呢,就干别的事去了。就乱序了呗。单线程没事,多线程是大事。
而且是避免不了的大事,

这是CPU自作主张这么干的!你再怎么限制编译器都没有用”(Herb Sutter)

这个线程的CPU把数据存入高速缓存,之后就干别的事了,如果接下来的事还得用到刚存的数据,CPU会从高速缓存中调还是主存?
当然会从cache,因为它知道在它这个“宇宙”(线程)里,它刚存的数据还在cache里呢,主存里的是旧的。可多线程下不只有它这个宇宙,别的宇宙或许正好也需要这个数据,然后它就只能从主存拿,因为别的宇宙可不知道cache里存的是新数据。结果是会出现什么情况。我这个线程已经把这个数据给写了,并且判断条件都通过了。你那线程,明明是同一时间相同的数据相同的判断条件,就是不通过。

我们再回到刚才的底层问题:如何实现seq
为什么要实现seq?为了保证每次读到的数据都是正确的数据。要读到的是最后更新这个数据的线程更新的数据。
而我刚才描述的那个问题正是为什么线程会读到未更新的数据!
也就是说我只要保证线程读到的是实时的数据就行了。
1.那也就是说,seq应该是可以保证store时CPU一口气彻底的存到主存!load()用seq也是从主存load()!(猜测1)
这两个过程是原子的,所以其他线程再读时读到的都会是新的。(话说真的可以这么保证么)
2.那relaxed就是正常的cache中有就从cache中读,没有就从主存读,写也是正常先写入cache,逐级到主存。(猜测2)
3.release和acquire就有意思了。release应该是用seq的方式一口气存到主存,
acquire是从主存load()。(猜测3)

我的猜测明显有漏洞,过两日再谈。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值