Golang并发陷阱
文章目录
【注意】最后更新于 May 23, 2021,文中内容可能已过时,请谨慎使用。
在大多数的语言里,赋值操作是原子性(不可拆分)的,但是Golang中并不是,Golang里很多类型的赋值操作是分多走的,而多步走就会导致在不同协程里操作同一个变量会出现异常,那么哪些类型的变量是并发赋值安全的,哪些是不安全的呢?
先说答案
Golang 中数据类型可以分类两大类:基本数据类型和复合数据类型。
基本数据类型有:字节型(安全),布尔型(安全)、整型(安全)、浮点型(安全)、字符型(安全)、复数型(不安全)、字符串(不安全)。
复合数据类又可细分为如下三类:
-
非引用类型:数组(不安全)、结构体(不安全);
-
引用类型:指针(安全)、切片(不安全)、字典(不安全)、通道(不安全)、函数(安全);
-
接口(不安全)。
并发不安全的原因
只有一个原因就是多步操作导致的,下面拿最多人搞错的字符串举例:
|
|
上面的代码很容易能看懂:一个协程不断地给s赋值ab,一个协程给s赋值abc,如果s既不等于ab也不等于abc就会跳出循环。
不了解Golang的人可能会想,不可能会出现既不是ab也不是abc的情况,然而事实是很容易就进入这个情况。
要解释这个就得去看源码包src/runtime/string.go里string的底层结构:
|
|
str 为字符串的首地址
len 为字符串的长度(单位字节)
因为string 底层结构是个 struct,还有两个字段,并发操作下经常会出现给str赋值了,另一个给len赋值了,就会出现str=ab,len=3,这样取出来的就不是ab了,也不是abc(当然有一定几率是abc,得看str地址下的第三个是不是刚好是c)
怎么并发安全赋值
看了上面的例子,很多小伙伴可能会想:天呐,还好看了这篇文章,不然出了线上安全事故可就惨了,这bug谁能找到啊!
那么如果出现并发操作同一个变量,如何保证并发安全呢?
加锁
加锁的例子有很多,最常见的就是sync.RWMutex,当然不同类型的变量可以使用其他第三方(或官方)的包,下面列举几个,就不一一列代码了:
- sync.RWMutex
- sync.Map
- concurrent-map
- atomic.Value
使用channel
除了加锁的方式,就剩下channel了,这里就要介绍下Golang的并发座右铭,在《Effective Go》的channel介绍中写到:
Share memory by communicating, don’t communicate by sharing memory.
通过通信共享内存,而不是通过共享内存而通信。
上面的话的意思是面对并发问题,你首先想到的应该是channel,因为channel是线程安全的并且不会有数据冲突,比锁好用,但要使用channel还是锁需要具体分析,并不是说channel可以替代锁,因为有些场景channel的性能并不比锁来得高效(事实上大多数情况下锁更高效,感兴趣的同学可以自己试一下,希望有一天Golang会把channel优化到我把这个括号去掉)
延伸阅读
《Go 语言标准库中 atomic.Value 的前世今生》
《Go sync.Map 实现》–此文详细分析了sync.Map的实现原理
《Mutex Or Channel》–mutex和channel如何选择
《Atomic、Mutex、Channel比较》–这三者的性能比较
文章作者 sunhuawei
上次更新 2021-05-23