Go语言核心36讲(Go语言实战与应用七)--学习笔记

2021年11月22日 阅读数:3
这篇文章主要向大家介绍Go语言核心36讲(Go语言实战与应用七)--学习笔记,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

29 | 原子操做(上)

咱们在前两篇文章中讨论了互斥锁、读写锁以及基于它们的条件变量,先来总结一下。git

互斥锁是一个颇有用的同步工具,它能够保证每一时刻进入临界区的 goroutine 只有一个。读写锁对共享资源的写操做和读操做则区别看待,并消除了读操做之间的互斥。github

条件变量主要是用于协调想要访问共享资源的那些线程。当共享资源的状态发生变化时,它能够被用来通知被互斥锁阻塞的线程,它既能够基于互斥锁,也能够基于读写锁。固然了,读写锁也是一种互斥锁,前者是对后者的扩展。安全

经过对互斥锁的合理使用,咱们可使一个 goroutine 在执行临界区中的代码时,不被其余的 goroutine 打扰。不过,虽然不会被打扰,可是它仍然可能会被中断(interruption)。并发

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

咱们已经知道,对于一个 Go 程序来讲,Go 语言运行时系统中的调度器会恰当地安排其中全部的 goroutine 的运行。不过,在同一时刻,只可能有少数的 goroutine 真正地处于运行状态,而且这个数量只会与 M 的数量一致,而不会随着 G 的增多而增加。函数

因此,为了公平起见,调度器老是会频繁地换上或换下这些 goroutine。换上的意思是,让一个 goroutine 由非运行状态转为运行状态,并促使其中的代码在某个 CPU 核心上执行。工具

换下的意思正好相反,即:使一个 goroutine 中的代码中断执行,并让它由运行状态转为非运行状态。学习

这个中断的时机有不少,任何两条语句执行的间隙,甚至在某条语句执行的过程当中都是能够的。ui

即便这些语句在临界区以内也是如此。因此,咱们说,互斥锁虽然能够保证临界区中代码的串行执行,但却不能保证这些代码执行的原子性(atomicity)。atom

在众多的同步工具中,真正可以保证原子性执行的只有原子操做(atomic operation)https://baike.baidu.com/item/%E5%8E%9F%E5%AD%90%E6%93%8D%E4%BD%9C/1880992?fr=aladdin 。原子操做在进行的过程当中是不容许中断的。在底层,这会由 CPU 提供芯片级别的支持,因此绝对有效。即便在拥有多 CPU 核心,或者多 CPU 的计算机系统中,原子操做的保证也是不可撼动的。操作系统

这使得原子操做能够彻底地消除竞态条件,并可以绝对地保证并发安全性。而且,它的执行速度要比其余的同步工具快得多,一般会高出好几个数量级。不过,它的缺点也很明显。

更具体地说,正是由于原子操做不能被中断,因此它须要足够简单,而且要求快速。

你能够想象一下,若是原子操做迟迟不能完成,而它又不会被中断,那么将会给计算机执行指令的效率带来多么大的影响。所以,操做系统层面只对针对二进制位或整数的原子操做提供了支持。

Go 语言的原子操做固然是基于 CPU 和操做系统的,因此它也只针对少数数据类型的值提供了原子操做函数。这些函数都存在于标准库代码包sync/atomic中。

我通常会经过下面这道题初探一下应聘者对sync/atomic包的熟悉程度。

咱们今天的问题是:sync/atomic包中提供了几种原子操做?可操做的数据类型又有哪些?

这里的典型回答是:

sync/atomic包中的函数能够作的原子操做有:加法(add)、比较并交换(compare and swap,简称 CAS)、加载(load)、存储(store)和交换(swap)。

这些函数针对的数据类型并很少。可是,对这些类型中的每个,sync/atomic包都会有一套函数给予支持。这些数据类型有:int3二、int6四、uint3二、uint6四、uintptr,以及unsafe包中的Pointer。不过,针对unsafe.Pointer类型,该包并未提供进行原子加法操做的函数。

此外,sync/atomic包还提供了一个名为Value的类型,它能够被用来存储任意类型的值。

问题解析

这个问题很简单,由于答案是明摆在代码包文档里的。不过若是你连文档都没看过,那也可能回答不上来,至少是没法作出全面的回答。

我通常会经过此问题再衍生出来几道题。下面我就来逐个说明一下。

第一个衍生问题 :咱们都知道,传入这些原子操做函数的第一个参数值对应的都应该是那个被操做的值。好比,atomic.AddInt32函数的第一个参数,对应的必定是那个要被增大的整数。但是,这个参数的类型为何不是int32而是*int32呢?

回答是:由于原子操做函数须要的是被操做值的指针,而不是这个值自己;被传入函数的参数值都会被复制,像这种基本类型的值一旦被传入函数,就已经与函数外的那个值毫无关系了。

因此,传入值自己没有任何意义。unsafe.Pointer类型虽然是指针类型,可是那些原子操做函数要操做的是这个指针值,而不是它指向的那个值,因此须要的仍然是指向这个指针值的指针。

只要原子操做函数拿到了被操做值的指针,就能够定位到存储该值的内存地址。只有这样,它们才可以经过底层的指令,准确地操做这个内存地址上的数据。

第二个衍生问题: 用于原子加法操做的函数能够作原子减法吗?好比,atomic.AddInt32函数能够用于减少那个被操做的整数值吗?

回答是:固然是能够的。atomic.AddInt32函数的第二个参数表明差量,它的类型是int32,是有符号的。若是咱们想作原子减法,那么把这个差量设置为负整数就能够了。

对于atomic.AddInt64函数来讲也是相似的。不过,要想用atomic.AddUint32和atomic.AddUint64函数作原子减法,就不能这么直接了,由于它们的第二个参数的类型分别是uint32和uint64,都是无符号的,不过,这也是能够作到的,就是稍微麻烦一些。

例如,若是想对uint32类型的被操做值18作原子减法,好比说差量是-3,那么咱们能够先把这个差量转换为有符号的int32类型的值,而后再把该值的类型转换为uint32,用表达式来描述就是uint32(int32(-3))。

不过要注意,直接这样写会使 Go 语言的编译器报错,它会告诉你:“常量-3不在uint32类型可表示的范围内”,换句话说,这样作会让表达式的结果值溢出。不过,若是咱们先把int32(-3)的结果值赋给变量delta,再把delta的值转换为uint32类型的值,就能够绕过编译器的检查并获得正确的结果了。

最后,咱们把这个结果做为atomic.AddUint32函数的第二个参数值,就能够达到对uint32类型的值作原子减法的目的了。

还有一种更加直接的方式。咱们能够依据下面这个表达式来给定atomic.AddUint32函数的第二个参数值:

^uint32(-N-1))

其中的N表明由负整数表示的差量。也就是说,咱们先要把差量的绝对值减去1,而后再把获得的这个无类型的整数常量,转换为uint32类型的值,最后,在这个值之上作按位异或操做,就能够得到最终的参数值了。

这么作的原理也并不复杂。简单来讲,此表达式的结果值的补码,与使用前一种方法获得的值的补码相同,因此这两种方式是等价的。咱们都知道,整数在计算机中是以补码的形式存在的,因此在这里,结果值的补码相同就意味着表达式的等价。

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

func main() {

	// 第二个衍生问题的示例。
	num := uint32(18)
	fmt.Printf("The number: %d\n", num)
	delta := int32(-3)
	atomic.AddUint32(&num, uint32(delta))
	fmt.Printf("The number: %d\n", num)
	atomic.AddUint32(&num, ^uint32(-(-3)-1))
	fmt.Printf("The number: %d\n", num)

	fmt.Printf("The two's complement of %d: %b\n", delta, uint32(delta)) // -3的补码。
	fmt.Printf("The equivalent: %b\n", ^uint32(-(-3)-1)) // 与-3的补码相同。
	fmt.Println()
}

总结

今天,咱们一块儿学习了sync/atomic代码包中提供的原子操做函数和原子值类型。原子操做函数使用起来都很是简单,但也有一些细节须要咱们注意。我在主问题的衍生问题中对它们进行了逐一说明。

在下一篇文章中,咱们会继续分享原子操做的衍生内容。

笔记源码

https://github.com/MingsonZheng/go-core-demo

知识共享许可协议

本做品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、从新发布,但务必保留文章署名 郑子铭 (包含连接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的做品务必以相同的许可发布。