在前一节内容Go语言中常见100问题-#68 Forgetting about possible side-effects with ...讲述了什么是数据竞争以及竞争产生的问题。本节内容将讨论切片话题并分析通过append向切片中添加元素是否存在竞争问题。提前透露一下,是否存在竞争视具体情况而定。
下面的例子中,先初始一个切片s,然后创建两个goroutine,在每个goroutine中通过append创建一个新的切片,并向里面添加一个元素。代码如下,完整可以运行的代码见(https://github.com/ThomasMing0915/100-go-mistakes-code/tree/main/69)
s := make([]int, 1)
go func() {
s1 := append(s, 1)
fmt.Println(s1)
}()
go func() {
s2 := append(s, 1)
fmt.Println(s2)
}()
上述程序是否存在数据竞争?答案是不存在。在分析原因之前,我们先来看看切片的基础知识,切片背后是有一个底层数组支撑的,它有两个属性:长度和容量。长度是切片中可用元素的数量,容量是底层数组的大小。当用append操作切片时,行为取决于切片是否已满(长度和容量相等)。如果已满,它会创建一个新的底层数组来添加元素,否则会添加元素到当前的底层数组中。上面的例子中,通过make创建了一个大小和容量都为1的切片s. 因此s已满,在每个goroutine中使用append向里面添加元素的时候,都会创建一个新的底层数组,不会改动s的底层数组,所以不存在数据竞争。
下面对上面的代码稍做改动,变化的地方是初始化s的时候,将创建一个长度为0但容量为1的切片。代码如下, 这种情况存在数据竞争吗?答案是存在数据竞争。这里切片s的长度和容量不等,即切片s还没有满,两个goroutine都尝试向切片s的底层数组的相同位置添加元素,导致了数据竞争产生。
s := make([]int, 0, 1)
// Same
运行上述程序,加入-race参数检查数据竞争,在控制台输出结果如下:
go run -race example2.go
[1]
==================
WARNING: DATA RACE
Write at 0x00c00001c0f8 by goroutine 8:
对于上面这种情况,如果希望两个goroutine都能够正常操作s, 并向里面添加元素,即怎么消除这里的数据竞争问题呢?一种解决方法是通过创建副本的方式,代码如下。
s := make([]int, 0, 1)
go func() {
sCopy := make([]int, len(s), cap(s))
copy(sCopy, s)
s1 := append(sCopy, 1)
fmt.Println(s1)
}()
go func() {
sCopy := make([]int, len(s), cap(s))
copy(sCopy, s)
s2 := append(sCopy, 1)
fmt.Println(s2)
}()
两个goroutine都是先创建s的副本,然后在副本上使用append进行元素追加操作,而不是在s上进行追加。这种将两个goroutine工作在隔离的数据上,防止产生数据竞争。
「NOTE:多个goroutine并发访问切片或map时,产生的数据竞争情况如何?」
在并发上下文环境中使用切片时,必须记住,在切片上使用append操作并不总是没有数据竞争的。具体行为依赖于切片是否已满,如果切片已满,则追加操作是无竞争的,否则如果切片没有满,多个goroutine可能会竞争更新相同的数组索引位置的数据,从而导致数据竞争。一般来说,我们不应该根据切片是否已满进行不同的编码实现,应该考虑到在并发应用程序中对共享切片使用append可能会导致数据竞争,因此应该避免它的产生。