1.什么是并发安全

并发安全就是程序在并发情况下执行的结果是正确的。
比如对一个变量简单的自增操作count++,在非并发下很好理解,而在并发情况下却容易出现预期之外的结果,这样的代码就是非并发安全的。
因为count++其实是分成两步执行的,当分成了两步执行,那么其他协程就可以趁着这个时间间隙作怪。
如一下 a b 两个协程同时 count++:

  1. count:= 1
  2. a > 读取count : 1
  3. b > 读取count : 1
  4. a > 计算count+1 : 2
  5. b > 计算count+1 : 2
  6. a > 赋值count : 2
  7. b > 赋值count : 2

这就会发生明明 a b 协程计算了两次,可结果还是 2。

2.struct 并发赋值安全吗

对一个简单变量的自增都会出现偏差,那么赋值一个更为复杂的结构体会不会有问题呢?
例如以下代码,在多协程的情况下,并发使用两个不同的值对结构体变量进行赋值,如果结构体成员出现异常情况, 那么说明并发出现了问题。

  1. type Test struct {
  2. X int
  3. Y int
  4. }
  5. func main() {
  6. var g Test
  7. for i := 0; i < 1000000; i++ {
  8. var wg sync.WaitGroup
  9. // 协程 1
  10. wg.Add(1)
  11. go func() {
  12. defer wg.Done()
  13. g = Test{1,2}
  14. }()
  15. // 协程 2
  16. wg.Add(1)
  17. go func(){
  18. defer wg.Done()
  19. g = Test{3,4}
  20. }()
  21. wg.Wait()
  22. // 赋值异常判断
  23. if !((g.X == 1 && g.Y == 2) || (g.X == 3 && g.Y == 4)) {
  24. fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
  25. break
  26. }
  27. }
  28. }

运行一次或多次,将出现赋值异常。

  1. concurrent assignment error, i=48714 g={X:1 Y:4}

结构体中有多个字段,协程 1 赋值了字段 X,协程 2 赋值了字段 Y,此时整个结构体既不是协程 1 想要的结果,也不是协程 2 想要的结果。可见 struct 赋值时,并不是原子操作,各个字段的赋值是独立的,在并发操作的情况下可能会出现异常。

3.如何保证并发赋值的安全性

Golang 早已想到该问题,并为我们提供一个开箱即用的类型 atomic.Value 来保证赋值的并发安全。

  1. // A Value provides an atomic load and store of a consistently typed value.
  2. // The zero value for a Value returns nil from Load.
  3. // Once Store has been called, a Value must not be copied.
  4. //
  5. // A Value must not be copied after first use.
  6. type Value struct {
  7. v interface{}
  8. }

让我们借助 atomic.Value 来完成对 struct 的安全并发赋值。

  1. func main() {
  2. var v atomic.Value
  3. for i := 0; i < 1000000; i++ {
  4. var wg sync.WaitGroup
  5. // 协程 1
  6. wg.Add(1)
  7. go func() {
  8. defer wg.Done()
  9. v.Store(Test{1,2})
  10. }()
  11. // 协程 2
  12. wg.Add(1)
  13. go func(){
  14. defer wg.Done()
  15. v.Store(Test{3,4})
  16. }()
  17. wg.Wait()
  18. // 赋值异常判断
  19. g := v.Load().(Test)
  20. if (g.X == 1 && g.Y == 2) || (g.X == 3 && g.Y == 4) {
  21. } else {
  22. fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
  23. break
  24. }
  25. }
  26. }

复制
上面执行将不会出现并发赋值异常的情况。

4.哪些类型并发赋值是安全的

我们已经知道了 struct 因为存在多个字段,赋值时各个字段时独立完成,所以并发不安全。那么对于 Golang 中其他的数据类型,并发赋值是安全的吗?
Golang 中数据类型可以分类两大类:基本数据类型和复合数据类型。
基本数据类型有:字节型,布尔型、整型、浮点型、字符型、复数型、字符串。
复合数据类型包括:指针、数组、切片、结构体、字典、通道、函数、接口。
复合数据类又可细分为如下三类: (1)非引用类型:数组、结构体; (2)引用类型:指针、切片、字典、通道、函数; (3)接口。
下面一一列举哪些数据类型是并发不安全的。

4.1 基本类型的并发赋值

4.1.1 字节型、布尔型、整型、浮点型、字符型(安全)

由于字节型、布尔型、整型、浮点型、字符型的位宽不会超过 64 位,在 64 位的指令集架构中可以由一条机器指令完成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的。
下面以浮点型为例进行测试。

  1. func main() {
  2. var g float64
  3. for i := 0; i < 1000000; i++ {
  4. var wg sync.WaitGroup
  5. // 协程 1
  6. wg.Add(1)
  7. go func() {
  8. defer wg.Done()
  9. g = 1.1
  10. }()
  11. // 协程 2
  12. wg.Add(1)
  13. go func(){
  14. defer wg.Done()
  15. g = 2.2
  16. }()
  17. wg.Wait()
  18. // 赋值异常判断
  19. if g != 1.1) && g != 2.2 {
  20. fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
  21. break
  22. }
  23. }
  24. }

上面个的测试代码对一个 float64 类型的变量进行并发赋值是没有问题的,其他类型读者可自行验证。

4.1.2 复数型(不安全)

按照上面的分析,因为复数型分为实部和虚部,两者的赋值是分开进行的,所以复数类型并发赋值是不安全的。

  1. func main() {
  2. var g complex64
  3. for i := 0; i < 1000000; i++ {
  4. var wg sync.WaitGroup
  5. // 协程 1
  6. wg.Add(1)
  7. go func() {
  8. defer wg.Done()
  9. g = complex(1,2)
  10. }()
  11. // 协程 2
  12. wg.Add(1)
  13. go func(){
  14. defer wg.Done()
  15. g = complex(3,4)
  16. }()
  17. wg.Wait()
  18. // 赋值异常判断
  19. if g != complex(1,2) && g != complex(3,4) {
  20. fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
  21. break
  22. }
  23. }
  24. }

运行输出:

  1. concurrent assignment error, i=131512 g=(1+4i)

注意:如果复数并发赋值时,有相同的虚部或实部,那么两个字段赋值就退化成一个字段,这种情况下时并发安全的。读者可自行验证。

4.1.3 字符串(不安全)

字符串在 Go 中是一个只读字节切片。
字符串有两个重要特点: (1)string 可以为空(长度为 0),但不会是 nil; (2)string对象不可以修改。
在源码包src/runtime/string.go我们可以找到 string 的底层数据结构:

  1. type stringStruct struct {
  2. str unsafe.Pointer
  3. len int
  4. }

其数据结构很简单: str 为字符串的首地址; len 为字符串的长度(单位字节); string 数据结构跟切片有些类似,只不过切片还有一个表示容量的成员,事实上 string 和字节切片间经常强制互转。
因为 string 底层结构是个 struct,前面已经讨论过 struct 并发赋值是不安全的,所以 string 的并发赋值同样是不安全。我们来验证一下。

  1. func main() {
  2. var s string
  3. for i := 0; i < 1000000; i++ {
  4. var wg sync.WaitGroup
  5. // 协程 1
  6. wg.Add(1)
  7. go func() {
  8. defer wg.Done()
  9. s = "ab"
  10. }()
  11. // 协程 2
  12. wg.Add(1)
  13. go func() {
  14. defer wg.Done()
  15. s = "abc"
  16. }()
  17. wg.Wait()
  18. // 赋值异常判断
  19. if s != "ab" && s != "abc" {
  20. fmt.Printf("concurrent assignment error, i=%v s=%v", i, s)
  21. break
  22. }
  23. }
  24. }

复制
运行输出:

  1. concurrent assignment error, i=509383 s=abi

并发赋值不出意料地出现了异常情况,推测正确。
从这里我们可以得到一个基本结论:只要底层结构是 struct 的类型,那么并发赋值都是不安全的。
注意不安全不代表一定发生错误。就是说不安全不代表任何并发赋值的情况下都会发生错误。比如上面测试代码循环次数少的情况下,很难出现出现异常情况。
不过我这里想说的不是次数的问题,因为次数多少是个概率的问题,我这里说的是和所要赋的值有关。只要不同的值满足一定特点,不管多少次并发,都是安全的。
为什么可以这么说呢,我们还是要回看 string 的底层数据结构。因为是两个字段,字节指针 str 和字符串长度 len,我们只要保证并发赋值情况下,两个字段的赋值正确就行。前面也说了,因为 struct 多个字段的赋值是独立,所以如果两个字段中只要有一个字段是不同的,那么并发赋值就变成了一个字段的并发赋值,这样就不会出现问题。
比如我们并发赋值两个等长度但内容不同的字符串,就不会有问题。验证如下:

  1. func main() {
  2. var s string
  3. for i := 0; i < 1000000; i++ {
  4. var wg sync.WaitGroup
  5. // 协程 1
  6. wg.Add(1)
  7. go func() {
  8. defer wg.Done()
  9. s = "123"
  10. }()
  11. // 协程 2
  12. wg.Add(1)
  13. go func() {
  14. defer wg.Done()
  15. s = "abc"
  16. }()
  17. wg.Wait()
  18. // 赋值异常判断
  19. if s != "123" && s != "abc" {
  20. fmt.Printf("concurrent assignment error, i=%v s=%v", i, s)
  21. break
  22. }
  23. }
  24. }

上面的代码,因为字符串 123 和 abc 是等长的,所以并发赋值不管循环多少次都是绝对的安全。因为 struct 赋值蜕变成了一个数值型指针的赋值。

4.2 复合数据类型的并发赋值

4.2.1 指针(安全)

指针是保存另一个变量的内存地址的变量。指针的零值为 nil。
因为是内存地址,所以位宽为 32位(x86平台)或 64位(x64平台),赋值操作由一个机器指令即可完成,不能被中断,所以也不会出现并发赋值不安全的情况。
这在上面讨论 string 等长不同值并发赋值时,已经验证没有问题。

4.2.2 函数(安全)

Go 函数可以像值一样传递。
Go 函数定义形式如下:

  1. func some_func_name(arguments) return_values

定义函数类型时去掉函数名:

  1. type TypeName func(arguments) return_values

其中 TypeName 是自定义的类型名称。
下面是一个函数类型的使用示例:

  1. package main
  2. import "fmt"
  3. func main() {
  4. // 定义函数类型的变量
  5. add := func(x, y int) int {
  6. return x + y
  7. }
  8. fmt.Println(add(1, 2))
  9. }
  10. // 函数作为形参
  11. func doOperation(fn func(int, int) int, x, y int) int {
  12. return fn(x, y)
  13. }

函数类型的变量赋值时,实际上赋的是函数地址,一条机器指令便可以完成,所以并发赋值是安全的。
我们使用unsafe.Sizeof()可以查看函数类型的宽度(字节)。

  1. type Add func(int, int) int
  2. var add Add
  3. fmt.Println(unsafe.Sizeof(add)) // 8

下面验证一下函数变量并发赋值的安全性。

  1. type Add func(int, int) int
  2. func main() {
  3. var g Add
  4. var i int
  5. for ; i < 10000000; i++ {
  6. var wg sync.WaitGroup
  7. // 协程 1
  8. wg.Add(1)
  9. go func() {
  10. defer wg.Done()
  11. g = func(x, y int) int {
  12. return x + y
  13. }
  14. }()
  15. // 协程 2
  16. wg.Add(1)
  17. go func() {
  18. defer wg.Done()
  19. g = func(x, y int) int {
  20. return x - y
  21. }
  22. }()
  23. wg.Wait()
  24. // 赋值异常判断
  25. if !(g(1, 1) == 2 || g(1, 1) == 0) {
  26. fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
  27. break
  28. }
  29. }
  30. if i == 10000000 {
  31. fmt.Println("no error")
  32. }
  33. }

运行输出:

  1. no error

4.2.2 数组、切片、字典、通道、接口(不安全)

数组、切片、字典、通道、接口,这些复合类型,除了数组,其他底层数据结构都是 struct,所以并发都不是安全的,当然数组并发赋值也是不安全的。
下面的讲解不会对所有类型一一验证,不过相关的底层数据我们应该着重了解一下。

数组

array 是相同类型值的集合,数组的长度是其类型的一部分。
数组赋值和传参都会拷贝整个数组的数据,所以数组不是引用类型。
数组的底层数据结构就是其本身,是一个相同类型不同值的顺序排列。所以如果数组位宽不大于 64 位且是 2 的整数次幂(8,16,32,64),那么其并发赋值其实也是安全的,只不过这个大部分情况并非如此,所以其并发赋值是不安全的。
下面以字节数组为例,看下位宽不大于 64 位的并发赋值安全的情况。

  1. func main() {
  2. var g [4]byte
  3. var i int
  4. for ; i < 10000000; i++ {
  5. var wg sync.WaitGroup
  6. // 协程 1
  7. wg.Add(1)
  8. go func() {
  9. defer wg.Done()
  10. g = [...]byte{1, 2, 3, 4}
  11. }()
  12. // 协程 2
  13. wg.Add(1)
  14. go func() {
  15. defer wg.Done()
  16. g = [...]byte{3, 4, 5, 6}
  17. }()
  18. wg.Wait()
  19. // 赋值异常判断
  20. if !(g == [...]byte{1, 2, 3, 4} || g == [...]byte{3, 4, 5, 6}) {
  21. fmt.Printf("concurrent assignment error, i=%v g=%+v", i, g)
  22. break
  23. }
  24. }
  25. if i == 10000000 {
  26. fmt.Println("no error")
  27. }
  28. }

运行输出:

  1. no error

可以看到,位宽为 32 位的数组 [4]byte,虽然有四个元素,但是赋值时由一条机器指令完成,所以也是原子操作。
如果你把字节数组的长度换成下面这样子,即使没有超过 64 位,也需要多条指令完成赋值,因为 CPU 中并没有这样位宽的寄存器,需要拆分为多条指令来完成。

  1. [3]byte
  2. [5]byte
  3. [7]byte

切片

slice 也是相同类型值的集合,只不过切片是动态调整大小的,内部是对数组的引用,相当于动态数组。如上所述,数组的大小是固定的,因此切片为数组提供了更灵活的接口。
切片是一种引用类型,它内部由三个字段表示:

  • 数组地址
  • 数组长度
  • 容量大小

在源码包src/runtime/slice.go我们可以找到切片的底层数据结构:

  1. type slice struct {
  2. array unsafe.Pointer
  3. len int
  4. cap int
  5. }

因为其是一个 struct,所以并发赋值是不安全的,这里不再以代码验证。

字典

map 是经常被使用的内置 key-value 型容器,是一个同类型元素的无序组,元素通过另一类型唯一键进行索引。
map 的底层结构也是一个 struct,定义于src/runtime/map.go:

  1. // A header for a Go map.
  2. type hmap struct {
  3. // Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
  4. // Make sure this stays in sync with the compiler's definition.
  5. count int // # live cells == size of map. Must be first (used by len() builtin)
  6. flags uint8
  7. B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
  8. noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
  9. hash0 uint32 // hash seed
  10. buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
  11. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
  12. nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
  13. extra *mapextra // optional fields
  14. }

map 并发读写会引发 panic,一般使用读写锁 sync.RWMutex 来保证安全。

通道

channel 在 goroutine 之间提供同步和通信。您可以将其视为 goroutines 通过其发送值和接收值的管道。操作符<-用于发送或接收数据,箭头方向指定数据流的方向。

  1. ch <- val // Sending a value present in var variable to channel
  2. val := <-cha // Receive a value from the channel and assign it to val variable

因为 channel 通常用法是初始化后作为共享变量在 goroutine 之间提供同步和通信,很少会发生赋值,就是把一个 channel 赋给另一个 channel,所以这里就不过多讨论其并发赋值的安全性。如果真的有这种情况,那么只要知道其底层数据结构是个 struct,并发赋值时不安全的即可。
关于 channel 的底层数据接口可在 Go 源码src\runtime\chan.go。

  1. type hchan struct {
  2. qcount uint // total data in the queue
  3. dataqsiz uint // size of the circular queue
  4. buf unsafe.Pointer // points to an array of dataqsiz elements
  5. elemsize uint16
  6. closed uint32
  7. elemtype *_type // element type
  8. sendx uint // send index
  9. recvx uint // receive index
  10. recvq waitq // list of recv waiters
  11. sendq waitq // list of send waiters
  12. // lock protects all fields in hchan, as well as several
  13. // fields in sudogs blocked on this channel.
  14. //
  15. // Do not change another G's status while holding this lock
  16. // (in particular, do not ready a G), as this can deadlock
  17. // with stack shrinking.
  18. lock mutex
  19. }

复制
关于 channel 的用法和实现原理,感兴趣的同学可自行查阅资料探究,这里不再赘述。

接口

接口是 Go 中的一个类型,它是方法的集合。实现接口的所有方法的任何类型都属于该接口类型。接口的零值为 nil。
定义一个接口类型的变量后,如果具体类型实现了接口的所有方法,我们可以将任何具体类型的值赋给这个变量。
实际上 Go 中的接口有个特殊情况,就是空接口,其不包含任何方法。因此,默认情况下,所有具体类型都实现空接口。
如果编写的函数接受空接口,则可以向该函数传递任何类型。

  1. package main
  2. import "fmt"
  3. func main() {
  4. test("thisisstring")
  5. test("10")
  6. test(true)
  7. }
  8. func test(a interface{}) {
  9. fmt.Printf("(%v, %T)\n", a, a)
  10. }

运行输出:

  1. (thisisstring, string)
  2. (10, string)
  3. (true, bool)

因为存在两种类型的接口,包含方法的非空接口和不包含任何方法的空接口,所以在底层实现上使用runtime.iface表示非空接口,使用runtime.eface表示空接口 interface{}。
在 Go 源码中 runtime 包下,我们可以找到 runtime.iface 和 runtime.eface 的定义。

  1. type iface struct { // 16 字节
  2. tab *itab
  3. data unsafe.Pointer
  4. }

这个结构体中有指向原始数据的指针 data 和 runtime.itab。
runtime.itab 结构体是接口类型的核心组成部分,每一个 runtime.itab 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter 和 _type 两个字段表示:

  1. type itab struct { // 32 字节
  2. inter *interfacetype
  3. _type *_type
  4. hash uint32
  5. _ [4]byte
  6. fun [1]uintptr
  7. }

除了 inter 和 _type 两个用于表示类型的字段之外,上述结构体中的另外两个字段也有自己的作用: hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致; fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的。

  1. type eface struct { // 16 字节
  2. _type *_type
  3. data unsafe.Pointer
  4. }

由于 interface{} 类型不包含任何方法,所以它的结构也相对来说比较简单,只包含指向底层数据和类型的两个指针。从上述结构我们也能推断出 Go 语言的任意类型都可以转换成 interface{}。
其中runtime._type是 Go 语言类型的运行时表示。下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。

  1. type _type struct {
  2. size uintptr
  3. ptrdata uintptr
  4. hash uint32
  5. tflag tflag
  6. align uint8
  7. fieldAlign uint8
  8. kind uint8
  9. equal func(unsafe.Pointer, unsafe.Pointer) bool
  10. gcdata *byte
  11. str nameOff
  12. ptrToThis typeOff
  13. }

size 字段存储了类型占用的内存空间,为内存空间的分配提供信息; hash 字段能够帮助我们快速确定类型是否相等; equal 字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的。
我们只需要对 runtime._type 结构体中的字段有一个大体的概念,不需要详细理解所有字段的作用和意义。
根据上面对接口底层结构的分析,我们可以得出如下结论:
接口底层数据结构包含两个字段,相互赋值时如果是相同具体类型不同值并发赋给一个接口,那么只有一个字段 data 的值是不同的,此时退化成指针的并发赋值,所以是安全的。但如果是不同具体类型的值并发赋给一个接口,那么并引发 panic。
不同具体类型并发赋值接口非安全验证如下:

  1. func main() {
  2. var g interface{}
  3. var i int
  4. for ; i < 10000000; i++ {
  5. var wg sync.WaitGroup
  6. // 协程 1
  7. wg.Add(1)
  8. go func() {
  9. defer wg.Done()
  10. g = "a"
  11. }()
  12. // 协程 2
  13. wg.Add(1)
  14. go func() {
  15. defer wg.Done()
  16. g = 1
  17. }()
  18. wg.Wait()
  19. // 赋值异常判断
  20. v1, _ := g.(string)
  21. v2, _ := g.(int)
  22. if !(v1 == "a" || v2 == 1) {
  23. fmt.Printf("concurrent assignment error, i=%v g=%v ", i, g)
  24. break
  25. }
  26. }
  27. if i == 10000000 {
  28. fmt.Println("no error")
  29. }
  30. }

运行输出:

  1. unexpected fault address 0x1fffffa8
  2. fatal error: fault
  3. [signal 0xc0000005 code=0x0 addr=0x1fffffa8 pc=0x5f5585]
  4. ...

把上面的示例代码中协议 1 中的字符串换成一个 int 值,那么并发是安全的。

  1. func main() {
  2. var g interface{}
  3. var i int
  4. for ; i < 10000000; i++ {
  5. var wg sync.WaitGroup
  6. // 协程 1
  7. wg.Add(1)
  8. go func() {
  9. defer wg.Done()
  10. g = 0
  11. }()
  12. // 协程 2
  13. wg.Add(1)
  14. go func() {
  15. defer wg.Done()
  16. g = 1
  17. }()
  18. wg.Wait()
  19. // 赋值异常判断
  20. v1, _ := g.(int)
  21. v2, _ := g.(int)
  22. if !(v1 == 0 || v2 == 1) {
  23. fmt.Printf("concurrent assignment error, i=%v g=%v ", i, g)
  24. break
  25. }
  26. }
  27. if i == 10000000 {
  28. fmt.Println("no error")
  29. }
  30. }

运行输出:

  1. no error

5.小结

Go 多协程并发的场景无处不在,并发对同一变量的赋值也是经常遇到。本文尝试探讨了 Go 中所有类型并发赋值的安全性。
(1)由一条机器指令完成赋值的类型并发赋值是安全的,这些类型有:字节型,布尔型、整型、浮点型、字符型、指针、函数。
(2)数组由一个或多个元素组成,大部分情况并发不安全。注意:当位宽不大于 64 位且是 2 的整数次幂(8,16,32,64),那么其并发赋值是安全的。
(3)struct 或底层是 struct 的类型并发赋值大部分情况并发不安全,这些类型有:复数、字符串、 数组、切片、字典、通道、接口。注意:当 struct 赋值时退化为单个字段由一个机器指令完成赋值时,并发赋值又是安全的。这种情况有: (a)实部或虚部相同的复数的并发赋值; (b)等长字符串的并发赋值; (c)同长度同容量切片的并发赋值; (d)同一种具体类型不同值并发赋给接口。

原文:https://www.cnblogs.com/sunsky303/p/17082018.html