Go 语言核心 36 讲
郝林
《Go 并发编程实战》作者,前轻松筹大数据负责人
79610 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 55 讲
Go 语言核心 36 讲
15
15
1.0x
00:00/00:00
登录|注册

29 | 原子操作(上)

sync/atomic.Value
unsafe.Pointer
uintptr
uint64
uint32
int64
int32
交换(swap)
存储(store)
加载(load)
比较并交换(CAS)
加法(add)
原子加法操作和原子减法操作
原子操作函数的参数类型
可操作的数据类型
原子操作函数
数据类型
执行速度
并发安全性
CPU支持
衍生问题
sync/atomic包
原子操作
调度器
原子性执行与原子操作
条件变量
读写锁
互斥锁
Go语言的原子操作

该思维导图由 AI 生成,仅供参考

我们在前两篇文章中讨论了互斥锁、读写锁以及基于它们的条件变量,先来总结一下。
互斥锁是一个很有用的同步工具,它可以保证每一时刻进入临界区的 goroutine 只有一个。读写锁对共享资源的写操作和读操作则区别看待,并消除了读操作之间的互斥。
条件变量主要是用于协调想要访问共享资源的那些线程。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程,它既可以基于互斥锁,也可以基于读写锁。当然了,读写锁也是一种互斥锁,前者是对后者的扩展。
通过对互斥锁的合理使用,我们可以使一个 goroutine 在执行临界区中的代码时,不被其他的 goroutine 打扰。不过,虽然不会被打扰,但是它仍然可能会被中断(interruption)。

前导内容:原子性执行与原子操作

我们已经知道,对于一个 Go 程序来说,Go 语言运行时系统中的调度器会恰当地安排其中所有的 goroutine 的运行。不过,在同一时刻,只可能有少数的 goroutine 真正地处于运行状态,并且这个数量只会与 M 的数量一致,而不会随着 G 的增多而增长。
所以,为了公平起见,调度器总是会频繁地换上或换下这些 goroutine。换上的意思是,让一个 goroutine 由非运行状态转为运行状态,并促使其中的代码在某个 CPU 核心上执行。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

Go语言的原子操作是一种能够保证原子性执行的同步工具,通过`sync/atomic`包提供的函数,可以对少数数据类型进行原子操作。这些函数包括加法、比较并交换、加载、存储和交换,针对的数据类型有`int32`、`int64`、`uint32`、`uint64`、`uintptr`和`unsafe.Pointer`。在使用原子操作函数时,需要传入被操作值的指针,而不是值本身。此外,原子加法操作函数也可以用于原子减法,对于无符号类型的值,可以通过转换类型或使用按位异或操作来实现原子减法。通过对原子操作函数的理解,可以更好地利用Go语言的并发特性,确保并发安全性并提高执行速度。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Go 语言核心 36 讲》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(27)

  • 最新
  • 精选
  • hd167
    “原子操作在进行的过程中是不允许中断的。在底层,这会由 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核心,它们页不可能并行进行。

    2020-01-10
    5
    29
  • poettian
    看到这里有点不理解,CAS 是两个步骤:比较和交换,可以使其成为一个原子操作。那像 load、store 这些都是单次操作呀,比如:a = 1,这种操作本身不是原子性的吗?这种赋值 和 atomic.Load* 方法有什么区别呢?

    作者回复: CAS本身包含两个动作:比较和交换。如果变量的值与我们期望的值相等,那么就用新的值替换,否则就不换。这个过程会保证原子性。这也是唯一一个包含多个步骤的原子性操作。 即使像 a = 1 这种简单的赋值操作也并不一定能够一次完成。如果右边的值的存储宽度超出了计算机的字宽,那么实际的步骤就会多于一个(或者说底层指令多于一个)。比如,你计算机是32位的,但是你要把一个Int64类型的数赋给变量a,那么底层指令就肯定多于一个。在这种情况下,多个底层指令的执行期间是可以被打断的,也就是说CPU在这时可以被切换到别的任务上。如果新任务恰巧要读写这个变量a,那么就会出现值不完整的问题。况且,就算是 a = 1,操作系统和CPU也都不保证这个操作一定不会被打断。只要被打断,就很有可能出现并发访问上的问题,并发安全性也就被破坏了。 所以说,当有多个goroutine在并发的读写这个变量的时候,它们之间就可能会造成干扰。总之,这种操作不是原子性,并发安全性也无法得到保障。 atomic包下的函数会借助操作系统级别的原子指令保障操作的原子性,即使计算机的CPU有多个核心或者有多个CPU,也是如此。

    2020-05-30
    2
    24
  • ArtistLu
    老师 “转换为uint32类型的值,最后,在这个值之上做按位异或操作” 文中题的异或对吗?我查了下go里面^一个操作数表示取反。

    作者回复: 原文是对的,只不过有一段隐含的话没有写上。如下。 如果与一元操作 ^ 联结的唯一操作数的类型是无符号的整数类型,那么这一操作就相当于对这个操作数和其整数类型的最大值进行二元的按位异或操作。例如: ^uint8(1) = 254 00000001 ^ 11111111 = 11111110

    2020-08-12
    15
  • 黑客不够黑
    "不过,在同一时刻,只可能有少数的 goroutine 真正地处于运行状态,并且这个数量只会与 M 的数量一致,而不会随着 G 的增多而增长。" 个人感觉同时运行的g应该是和p数量相等

    作者回复: 不一定,P 把 G 交给 M 就不管了,调度器发现还有 M 闲着就会继续从那些 P 那边拿 G。

    2019-09-25
    10
  • 授人以🐟,不如授人以渔
    真正运行的 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调度器的效率。

    2021-05-09
    7
  • 拂尘
    继续 poettian 的问题。a=1本身不是原子性的吗? @郝老师 你的回答里只是说了超出字宽的情况。那如果对没有超出字宽的情况,是不是对于多cpu之间的本地三级缓存同步,也是a=1不是原子性的一种case,我这样理解对吗?

    作者回复: 对的。这还连带着内存访问的问题。如果没有CPU级别的原子指令的保护,一个CPU核心根本就阻止不了其他CPU核心在同一期间访问同一个块内存或者同一块缓存。

    2020-09-01
    5
  • 涛声依旧
    原子操作虽然快,但使用场景有限;锁虽然使用麻烦,但使用场景较多;这是我对锁与原子操作的理解;

    作者回复: 是的,鱼和熊掌难以兼得。

    2020-03-28
    3
    5
  • 黑客不够黑
    “这个中断的时机有很多,任何两条语句执行的间隙,甚至在某条语句执行的过程中都是可以的。即使这些语句在临界区之内也是如此“ 我对这段话里的临界区内也能产生中断有些疑问,我查阅相关资料后理解为,CPU提供了一些原子操作机制,os或者语言api使用这些原子操作实现了锁,使用锁能保证更大范围的原子性,也就是说我理解的临界区也是一组不可中断的操作,也是具有原子性的;而且单核上进入临界区会关中断,离开临界区会开中断;所以郝大我这里是不是理解有误?

    作者回复: 临界区是指互斥锁保护的区域。锁只能保证操作上的互斥,或者说串行,但不能保证不被中断。“互斥”和“不中断”是两个不同的概念。锁保护的代码没有原子性一说,在执行的过程中是有可能被中断的,即可能会被撤下CPU,转而运行其他并发的代码。 互斥锁里面会用到原子操作,但那只是其中的一个或几个步骤而已。原子操作只能针对原始的内存地址来做,其中的一个原因是它对CPU的性能影响巨大。也正因为如此,原子操作根本无法顾及某段代码这么大颗粒度的东西。

    2019-12-24
    3
    4
  • BOB
    郝老师您好,在前面讲基准测试的时候,说过go运行时最多同时调度 P 个goroutine,这一篇说同时运行的goroutine数目最大为M。。这两句话是否矛盾?

    作者回复: 调度和同时运行是两码事啊。当goroutine在M上运行并且需要暂时挂起时,P就不去管它了,转而去调度其他的可运行goroutine,等前面那个goroutine从等待或挂起状态恢复成可运行状态时,调度器再给它找P或者放入全局可运行G队列等待调度。 所以,在高并发场景,正在运行的goroutine数量,肯定比正在被调度的goroutine数量多啊。

    2021-01-27
    1
  • 曼巴
    原子操作的意义,老师的回答,摘录出来了。 即使像 a = 1 这种简单的赋值操作也并不一定能够一次完成。如果右边的值的存储宽度超出了计算机的字宽,那么实际的步骤就会多于一个(或者说底层指令多于一个)。比如,你计算机是32位的,但是你要把一个Int64类型的数赋给变量a,那么底层指令就肯定多于一个。在这种情况下,多个底层指令的执行期间是可以被打断的,也就是说CPU在这时可以被切换到别的任务上。如果新任务恰巧要读写这个变量a,那么就会出现值不完整的问题。况且,就算是 a = 1,操作系统和CPU也都不保证这个操作一定不会被打断。只要被打断,就很有可能出现并发访问上的问题,并发安全性也就被破坏了。

    作者回复: 棒 :b

    2022-02-19
收起评论
显示
设置
留言
27
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部