append()方法_Go语言中的Append不总是安全

94fce277e75d6407055264f89dd75369.png

文章翻译自: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的内存空间的情况。

30ce2f7362f857c0862596fe86910800.png

Go在运行中注意到:已经没有地方去存放"hello", "world"或"goodbye", "bob"。于是为y,z

创建内存空间。多个goroutines去读取内存,竞态并不会发生,因为x并没有改变。

7909718c214690323393985414487037.png

而在产生错误的代码却不一样。

0eb635753825950171924e703a46c1f6.png

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()
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值