文章翻译自:Go’s append is not always thread safe
翻译: Takayamaaren
在Go中,经常能够看见向slice执行Append这种线程不安全的行为的bug。下面是一个简单的单元测试的例子:两个goroutines向同一个slice添加。
运行测试,并添加-race的flag,能够正常通过。
package main
import (
"sync"
"testing"
)
func TestAppend(t *testing.T) {
x := []string{"start"}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
y := append(x, "hello", "world")
t.Log(cap(y), len(y))
}()
go func() {
defer wg.Done()
z := append(x, "goodbye", "bob")
t.Log(cap(z), len(z))
}()
wg.Wait()
}
然后修改一下 x := []string{"start"} ,让x拥有更多的空间。
package main
import (
"testing"
"sync"
)
func TestAppend(t *testing.T) {
x := make([]string, 0, 6)
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
y := append(x, "hello", "world")
t.Log(len(y))
}()
go func() {
defer wg.Done()
z := append(x, "goodbye", "bob")
t.Log(len(z))
}()
wg.Wait()
}
运行测试并添加-race的flag,我们就会注意到出现了竞态的情况。
< go test -race .
==================
WARNING: DATA RACE
Write at 0x00c4200be060 by goroutine 8:
_/tmp.TestAppend.func2()
/tmp/main_test.go:20 +0xcb
Previous write at 0x00c4200be060 by goroutine 7:
_/tmp.TestAppend.func1()
/tmp/main_test.go:15 +0xcb
Goroutine 8 (running) created at:
_/tmp.TestAppend()
/tmp/main_test.go:18 +0x14f
testing.tRunner()
/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16d
Goroutine 7 (running) created at:
_/tmp.TestAppend()
/tmp/main_test.go:13 +0x105
testing.tRunner()
/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16d
==================
==================
WARNING: DATA RACE
Write at 0x00c4200be070 by goroutine 8:
_/tmp.TestAppend.func2()
/tmp/main_test.go:20 +0x11a
Previous write at 0x00c4200be070 by goroutine 7:
_/tmp.TestAppend.func1()
/tmp/main_test.go:15 +0x11a
Goroutine 8 (running) created at:
_/tmp.TestAppend()
/tmp/main_test.go:18 +0x14f
testing.tRunner()
/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16d
Goroutine 7 (finished) created at:
_/tmp.TestAppend()
/tmp/main_test.go:13 +0x105
testing.tRunner()
/usr/local/Cellar/go/1.10.2/libexec/src/testing/testing.go:777 +0x16d
==================
--- FAIL: TestAppend (0.00s)
main_test.go:16: 2
main_test.go:21: 2
testing.go:730: race detected during execution of test
FAIL
FAIL _/tmp 0.901s
分析一下
要理解为什么错误,先看一下之前的(没出问题)x的内存空间的情况。
Go在运行中注意到:已经没有地方去存放"hello", "world"或"goodbye", "bob"。于是为y,z
创建内存空间。多个goroutines去读取内存,竞态并不会发生,因为x并没有改变。
而在产生错误的代码却不一样。
Go在运行中发现这里有空间存放“hello”, “world”,另一个goroutine也发现了有空间存放“goodbye”, “bob”.在此,竞态就发生了,两个goroutine竞争地写入相同地方而且没有明确的顺序。
这是Go语言的一个feature,每次append在调用时不会申请分配。这允许用户可以循环的append并且不会破坏GC,缺点是:要避免多个goroutine向同一个slice添加。
为什么会有这样的误解
- x = append(x, ...)看起来像是取到了一个新的slice
- 大多数的函数返回值不会修改输入
- 通常情况下,append会返回一个新的slice
- 最致命的一点:以为append是只读的
Bug原因
如果append的第一个参数不是局部变量,这时就需要特别注意。
这个错误通常在:struct中的变量传递给当前函数append时出现。例如:在每一个请求中,struct有一些默认值并且被添加。当append至共享内存(变量)的时候需要注意。
解决方法
最简单的解决方式是:不要让共享的变量作为append的第一个参数。创建一个新的符合大小需要slice,用这个作为append的第一个参数。
下面是修改过后可以通过测试的例子,也可以用copy函数来实现。
package main
import (
"sync"
"testing"
)
func TestAppend(t *testing.T) {
x := make([]string, 0, 6)
x = append(x, "start")
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
y := make([]string, 0, len(x)+2)
y = append(y, x...)
y = append(y, "hello", "world")
t.Log(cap(y), len(y), y[0])
}()
go func() {
defer wg.Done()
z := make([]string, 0, len(x)+2)
z = append(z, x...)
z = append(z, "goodbye", "bob")
t.Log(cap(z), len(z), z[0])
}()
wg.Wait()
}