29 | 原子操作(上)
29 | 原子操作(上)
讲述:黄洲君
时长08:32大小3.92M
前导内容:原子性执行与原子操作
问题解析
总结
赞 16
提建议
精选留言(27)
- hd1672020-01-10“原子操作在进行的过程中是不允许中断的。在底层,这会由 CPU 提供芯片级别的支持,所以绝对有效。即使在拥有多 CPU 核心,或者多 CPU 的计算机系统中,原子操作的保证也是不可撼动的。”这句话不知道该怎么理解?是不是如果一个原子操作在进行中,这台计算机的其他cpu核心都不能进行相关操作? 举个例子,有两个Int32的变量a,b,如果一个线程x要对a做原子加法操作,另一个线程y想要对b做原子交换操作,多核cpu的话,线程x,y应该可以在不同的物理核心上同时进行操作吧? 但是如果只有一个Int32的变量c,线程x要对c做原子加法操作,线程y要对c做原子交换操作,多核cpu的 话可,线程x和线程y还可以在不同的物理核心上同时进行吗?展开
作者回复: 任何原子操作都是真对某一个共享资源而言的,即内存中的同一个小块区域以及相关的缓存。这个区域足够小,因此通常只用一个CPU指令就可以完成对它的读或写。 所以,这并不是说,一遇到原子操作,所有的CPU核心就都不工作了。你要理解“中断”这个词的真正含义。这里的细节还是很多的,涉及到cache line、多核CPU协调机制(几种方案)、LOCK原语(CPU不同方案不同)等等。一句两句说不太清楚,你可以自己去找资料看看。 总之,你可以理解为,针对不同共享资源的原子操作是有可能并行进行的。而针对相同共享资源的原子操作,即使存在多CPU核心,它们页不可能并行进行。
共 5 条评论27 - poettian2020-05-30看到这里有点不理解,CAS 是两个步骤:比较和交换,可以使其成为一个原子操作。那像 load、store 这些都是单次操作呀,比如:a = 1,这种操作本身不是原子性的吗?这种赋值 和 atomic.Load* 方法有什么区别呢?
作者回复: CAS本身包含两个动作:比较和交换。如果变量的值与我们期望的值相等,那么就用新的值替换,否则就不换。这个过程会保证原子性。这也是唯一一个包含多个步骤的原子性操作。 即使像 a = 1 这种简单的赋值操作也并不一定能够一次完成。如果右边的值的存储宽度超出了计算机的字宽,那么实际的步骤就会多于一个(或者说底层指令多于一个)。比如,你计算机是32位的,但是你要把一个Int64类型的数赋给变量a,那么底层指令就肯定多于一个。在这种情况下,多个底层指令的执行期间是可以被打断的,也就是说CPU在这时可以被切换到别的任务上。如果新任务恰巧要读写这个变量a,那么就会出现值不完整的问题。况且,就算是 a = 1,操作系统和CPU也都不保证这个操作一定不会被打断。只要被打断,就很有可能出现并发访问上的问题,并发安全性也就被破坏了。 所以说,当有多个goroutine在并发的读写这个变量的时候,它们之间就可能会造成干扰。总之,这种操作不是原子性,并发安全性也无法得到保障。 atomic包下的函数会借助操作系统级别的原子指令保障操作的原子性,即使计算机的CPU有多个核心或者有多个CPU,也是如此。
共 2 条评论21 - ArtistLu2020-08-12老师 “转换为uint32类型的值,最后,在这个值之上做按位异或操作” 文中题的异或对吗?我查了下go里面^一个操作数表示取反。
作者回复: 原文是对的,只不过有一段隐含的话没有写上。如下。 如果与一元操作 ^ 联结的唯一操作数的类型是无符号的整数类型,那么这一操作就相当于对这个操作数和其整数类型的最大值进行二元的按位异或操作。例如: ^uint8(1) = 254 00000001 ^ 11111111 = 11111110
14 - Richard2019-09-25"不过,在同一时刻,只可能有少数的 goroutine 真正地处于运行状态,并且这个数量只会与 M 的数量一致,而不会随着 G 的增多而增长。" 个人感觉同时运行的g应该是和p数量相等
作者回复: 不一定,P 把 G 交给 M 就不管了,调度器发现还有 M 闲着就会继续从那些 P 那边拿 G。
10 - 授人以🐟,不如授人...2021-05-09真正运行的 goroutine 数量应该是和 CPU 核心数是相等,毕竟同一时刻正处在运行状态的 goroutine 必须是运行在真实的 CPU 核心上的。老师我这样理解,是正确的吗?
作者回复: 首先你要明确,每个G(goroutine)都需要在某个M(系统线程)上运行,而每个M都需要在某个CPU核心中运行。所以: 1. 在同一时刻,Go调度器中的G有很多,但是正在运行的G的数量不会多于M数。如果当前所有的M都忙不过来了,那么Go调度器就会马上生成新的M。 2. 在某些时候,M的数量可能会超过CPU核心数。但是,每个CPU核心在同一时刻只能运行一个M。过多的M会加重CPU核心的负担。 下面正式回答你的问题: Goroutine不一定会与CPU核心数相等,这没有必要,也不容易控制。 Go语言调度模型中的P最好与CPU核心数相等。 因为一个P就代表着一条调度线。可以说,它会不断地把其队列中的G喂给M。反过来讲,每个M一旦饿了(处于空闲状态)就会向调度器要吃的(可运行的G)。 基于此,如果调度线太少,那么这个调度模型的效率就无法完全体现,一些CPU核心就会比较闲;但如果调度线太多,那调度器调度的可运行G又会导致很多M产生,而这些M又会多到CPU核心忙不过来(反而会影响效率)。 一个Goroutine在等待IO、等待计时器、等待锁的时候会处于等待状态。这个时候,当前的P就可以把这个goroutine与运行它的M分离了。之后,这个M就空闲了,就可以去寻找其他的G去运行了。 基于此,我们提交给Go调度器的goroutine可以很多,但是正在运行的goroutine会与M基本保持一致。这种平衡状态会由P的数量控制,所以说P在调度的过程中是至关重要的,会直接影响到Go调度器的效率。
5 - 鸠摩·智2020-09-01继续 poettian 的问题。a=1本身不是原子性的吗? @郝老师 你的回答里只是说了超出字宽的情况。那如果对没有超出字宽的情况,是不是对于多cpu之间的本地三级缓存同步,也是a=1不是原子性的一种case,我这样理解对吗?
作者回复: 对的。这还连带着内存访问的问题。如果没有CPU级别的原子指令的保护,一个CPU核心根本就阻止不了其他CPU核心在同一期间访问同一个块内存或者同一块缓存。
5 - Richard2019-12-24“这个中断的时机有很多,任何两条语句执行的间隙,甚至在某条语句执行的过程中都是可以的。即使这些语句在临界区之内也是如此“ 我对这段话里的临界区内也能产生中断有些疑问,我查阅相关资料后理解为,CPU提供了一些原子操作机制,os或者语言api使用这些原子操作实现了锁,使用锁能保证更大范围的原子性,也就是说我理解的临界区也是一组不可中断的操作,也是具有原子性的;而且单核上进入临界区会关中断,离开临界区会开中断;所以郝大我这里是不是理解有误?展开
作者回复: 临界区是指互斥锁保护的区域。锁只能保证操作上的互斥,或者说串行,但不能保证不被中断。“互斥”和“不中断”是两个不同的概念。锁保护的代码没有原子性一说,在执行的过程中是有可能被中断的,即可能会被撤下CPU,转而运行其他并发的代码。 互斥锁里面会用到原子操作,但那只是其中的一个或几个步骤而已。原子操作只能针对原始的内存地址来做,其中的一个原因是它对CPU的性能影响巨大。也正因为如此,原子操作根本无法顾及某段代码这么大颗粒度的东西。
共 3 条评论4 - 给力2020-03-29// AddUint32 atomically adds delta to *addr and returns the new value. // To subtract a signed positive constant value c from x, do AddUint32(&x, ^uint32(c-1)). // In particular, to decrement x, do AddUint32(&x, ^uint32(0)). func AddUint32(addr *uint32, delta uint32) (new uint32) 想要在无符号中减去有符号数。以上源码中提供了2种方式展开3
- 涛声依旧2020-03-28原子操作虽然快,但使用场景有限;锁虽然使用麻烦,但使用场景较多;这是我对锁与原子操作的理解;
作者回复: 是的,鱼和熊掌难以兼得。
共 3 条评论3 - 传说中的成大大2020-04-10如果我们先把int32(-3)的结果值赋给变量delta,再把delta的值转换为uint32类型的值,就可以绕过编译器的检查并得到正确的结果了。 我终于想明白了 是因为delta是个变量 类型是int32类型 所以编译器在编译的时候是不会报overflow的 但是如果写个字面量-3则是可以编译检查出来的共 1 条评论2
- BOB2021-01-27郝老师您好,在前面讲基准测试的时候,说过go运行时最多同时调度 P 个goroutine,这一篇说同时运行的goroutine数目最大为M。。这两句话是否矛盾?
作者回复: 调度和同时运行是两码事啊。当goroutine在M上运行并且需要暂时挂起时,P就不去管它了,转而去调度其他的可运行goroutine,等前面那个goroutine从等待或挂起状态恢复成可运行状态时,调度器再给它找P或者放入全局可运行G队列等待调度。 所以,在高并发场景,正在运行的goroutine数量,肯定比正在被调度的goroutine数量多啊。
1 - 曼巴2022-02-19原子操作的意义,老师的回答,摘录出来了。 即使像 a = 1 这种简单的赋值操作也并不一定能够一次完成。如果右边的值的存储宽度超出了计算机的字宽,那么实际的步骤就会多于一个(或者说底层指令多于一个)。比如,你计算机是32位的,但是你要把一个Int64类型的数赋给变量a,那么底层指令就肯定多于一个。在这种情况下,多个底层指令的执行期间是可以被打断的,也就是说CPU在这时可以被切换到别的任务上。如果新任务恰巧要读写这个变量a,那么就会出现值不完整的问题。况且,就算是 a = 1,操作系统和CPU也都不保证这个操作一定不会被打断。只要被打断,就很有可能出现并发访问上的问题,并发安全性也就被破坏了。展开
作者回复: 棒 :b
- 青山2021-12-28我有个问题既然add操作的是指针就表示对指针对应的值做操作为啥还要返回一个新值,直接获取操作的指针值就可以获得最新的值了,不需要让他重新返回一个新值把。
作者回复: 程序再获取这个变量的值也得用原子操作啊(如果要保护共享变量,就应该实施全面的保护),这里直接返回来不就省的再做一次原子操作了嘛。
- lesserror2021-08-17郝林老师,请问像demo63中的「forAndCAS1」函数 中的 go function 可以直接使用 外面的 变量 :num,又可以通过 传参的 形式 传递到 go function 中,例如 「forAndCAS2」函数。请问 这 两种 使用 方式 有什么区别 吗?
作者回复: 对于基本类型的值来说,通过传参在其他函数里使用的话,就意味着使用的是它们的副本。因为无论是参数的传入还是结果的返回,那些值都会经历浅表复制。 相反,如果直接在其他函数里使用基本类型的外部变量,那么就等于在使用它们的本体。如果直接使用本体,就要考虑并发安全问题了。
- AlexWillBeGood2021-03-29if newNum == 10 { break } newNum和10不一定相同
- AlexWillBeGood2021-03-29自旋锁1例子里面的
- 郭小菜2020-08-22异或是两个数的操作,应该表述为按位取反!
作者回复: 还真不是按位取反,我前两天已经回答过一次了。
共 2 条评论 - ArtistLu2020-08-12老师 请问下一个操作数异或咋个理解?异或不是两个操作数吗?
作者回复: 已经在你另一个问题下回复了。
- Tom2020-07-28清晰的认识到了锁与原子操作的区别。很多时候之所以会认为互斥锁管理的临界区是原子性的,是因为它是串行,在没有释放锁等情况下只有持有锁的那个线程一直在执行,表现出原子现象。 举个例子,如果临界区中有多条状态更新组成的原子块,第一 条状态更新语句执行完之后被中断释放了锁,另一个线程来执行看到了这个状态原子性就被打破了。 cpu这种原子性是硬件层面的支持,一般需要配合自旋来完成
- 亢(知行合一的路上)2020-06-04原子操作在多协程同时做计数时还是挺有用的。