概念
并发安全就是程序在并发情况下执行的结果都是正确的。
Go数据类型
Go中数据类型可以分为两大类:基本数据类型和复合数据类型。
基本数据类型:字节型,布尔型,整型,浮点型,复数型,字符串。
复合数据类型:数组,切片,指针,结构体,字典,通道,函数,接口
复合数据类又可细分为如下三类:
(1)非引用类型:数组,结构体
(2)引用类型:指针,切片,字典,通道,函数
(3)接口
类型与并发安全
(1)字节型,布尔型,整形,浮点型(取决于操作系统指令集)
由于他们的位宽不会超过64位,所以在64位的指令集架构中可以由一条机器指令完成,不存在被细分为更小的操作单位,所以这些类型的并发赋值是安全的,但是这个也跟操作系统的位数有关,比如int64在32位操作系统中,它的高32位和低32位是分开赋值的,此时是非并发安全的。
(2)复数型(不安全)
因为复数分为实部和虚部,两者的赋值时分开进行的,所以复数时非并发安全的。注意:如果复数并发赋值时,有相同的实部和虚部,那么两个字段的赋值就会退化为一个字段,这种情况下是并发安全的。
(3)字符串(不安全)
字符串在go中是一个只读字节切片,string有两个重要的特点:string可以为空(长度为0),但不会是nil;string对象不可以修改。string底层是一个结构体,包含两个字段:str为字符串的首地址指针,len为字符串的长度。只要底层是结构体类型,都不是并发安全的。但是如果只并发给结构体中的一个字段赋值,这样就是并发安全的。
(4)指针(安全)
指针是保存两一个变量的内存地址的变量,因为是内存地址,所以位宽为32位(x86平台)或64位(x64平台),赋值操作由一条机器指令即可完成,不能被中断,所以是并发安全的。
(5)结构体(不安全)
结构体中有多个字段,每个字段都是单独赋值的,在并发操作的情况下可能会出现问题。
(6)函数(安全)
函数类型的变量赋值时,实际上赋的是函数地址,一条机器指令便可完成,所以并发赋值是安全的。我们使用unsafe.Sizeof()可以查看函数类型的宽度(字节)。
(7)数组,切片,字典,通道,接口(不安全)
数组、切片、字典、通道、接口,这些复合类型,除了数组,其他底层数据结构都是 struct,所以并发都不是安全的,当然数组并发赋值也是不安全的。
数组
数组的底层数据结构就是其本身,是一个相同类型不同值的顺序排列。所以如果数组位宽不大于 64 位且是 2 的整数次幂(8,16,32,64),那么其并发赋值其实也是安全的,只不过这个大部分情况并非如此,所以其并发赋值是不安全的。
通道
因为 channel 通常用法是初始化后作为共享变量在 goroutine 之间提供同步和通信,很少会发生赋值,就是把一个 channel 赋给另一个 channel,所以这里就不过多讨论其并发赋值的安全性。如果真的有这种情况,那么只要知道其底层数据结构是个 struct,并发赋值时不安全的即可。
接口
接口底层数据结构包含两个字段,相互赋值时如果是相同具体类型不同值并发赋给一个接口,那么只有一个字段 data 的值是不同的,此时退化成指针的并发赋值,所以是安全的。但如果是不同具体类型的值并发赋给一个接口,那么并引发 panic。
小结
Go 多协程并发的场景无处不在,并发对同一变量的赋值也是经常遇到。本文尝试探讨了 Go 中所有类型并发赋值的安全性。
(1)由一条机器指令完成赋值的类型并发赋值是安全的,这些类型有:字节型,布尔型、整型、浮点型、字符型、指针、函数。
(2)数组由一个或多个元素组成,大部分情况并发不安全。注意:当位宽不大于 64 位且是 2 的整数次幂(8,16,32,64),那么其并发赋值是安全的。
(3)struct 或底层是 struct 的类型并发赋值大部分情况并发不安全,这些类型有:复数、字符串、 数组、切片、字典、通道、接口。注意:当 struct 赋值时退化为单个字段由一个机器指令完成赋值时,并发赋值又是安全的。这种情况有:
(a)实部或虚部相同的复数的并发赋值;
(b)等长字符串的并发赋值;
(c)同长度同容量切片的并发赋值;
(d)同一种具体类型不同值并发赋给接口。