go append 方法_Go的append操作是线程安全的吗

“ 根据golang中slice的数据结构可知,slice依托数组实现,在底层数组容量充足时,append操作不是只读操作,会将元素直接加入数组的空闲位置。因此,在多协程 对全局slice进行append操作时,会操作同一个底层数据,导致读写冲突”

下面我将介绍两个对切片执行append操作的例子。一个是线程安全的,一个是线程不安全的。然后分析线程不安全产生的原因以及对应的解决方案。

01

线程安全的例子

package mainimport (  "sync"  "fmt")func main() {    x := []string{"Start"} //初始化时,slice的容量为1    wg := sync.WaitGroup{}    wg.Add(2)    go func() {      defer wg.Done()      y := append(x, "Hello", "World")      fmt.Printf("y slice len:%d, cap:%d\n", len(y), cap(y))    }()    go func() {        defer wg.Done()        z := append(x, "Java", "Golang", "React")        fmt.Printf("z len:%d, cap:%d\n", len(z), cap(z))    }()    wg.Wait()}
在终端执行 go  run  -race main.go命令运行程序,发现正常执行,不存在数据竞争。

02

线程不安全(数据竞争)的例子

package mainimport (  "fmt"  "sync")func main() {    x := make([]string, 0, 6) //初始化时slice的容量为6    wg := sync.WaitGroup{}    wg.Add(2)    go func() {        defer wg.Done()        y := append(x, "Hello", "World")        fmt.Printf("y slice len:%d, cap:%d, value:%+v\n", len(y), cap(y), y)    }()    go func() {        defer wg.Done()        z := append(x, "Java", "Go", "React")        fmt.Printf("z slice len:%d, cap:%d, value:%+v\n", len(z), cap(z), z)    }()    wg.Wait()}
在终端执行 go  run  -race main.go命令运行程序,发现提示WARNING:DATA RACE,存在数据竞争。结果如下:
sh-3.2# go run -race main.goy slice len:2, cap:6, value:[Hello World]==================WARNING: DATA RACEWrite at 0x00c0000b4120 by goroutine 8:  main.TestAppendNotSafeThread.func2()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:89 +0xd7Previous write at 0x00c0000b4120 by goroutine 7:  main.TestAppendNotSafeThread.func1()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:83 +0xd7Goroutine 8 (running) created at:  main.TestAppendNotSafeThread()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:87 +0x12c  main.main()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:9 +0x2fGoroutine 7 (finished) created at:  main.TestAppendNotSafeThread()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:81 +0xee  main.main()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:9 +0x2f====================================WARNING: DATA RACEWrite at 0x00c0000b4130 by goroutine 8:  main.TestAppendNotSafeThread.func2()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:89 +0x145Previous write at 0x00c0000b4130 by goroutine 7:  main.TestAppendNotSafeThread.func1()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:83 +0x12cGoroutine 8 (running) created at:  main.TestAppendNotSafeThread()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:87 +0x12c  main.main()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:9 +0x2fGoroutine 7 (finished) created at:  main.TestAppendNotSafeThread()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:81 +0xee  main.main()      /Users/shaoyu.yang/htdocs/goproj/demo/main.go:9 +0x2f==================z slice len:3, cap:6, value:[Java Go React]Found 2 data race(s)exit status 66

03

根因分析

在分析根因之前,我们先来看下slice的数据结构
type slice struct {    array unsafe.Pointer    len   int    cap   int}
从结构上看slice很清晰,array指针指向底层数组,len标识切片长度,cap表示底层数组容量.例如, slice := make([]int, 4, 8)语句所创建的slice数据结构如下图所示:

b94d510eb032c44ae5ce657f44c0e6ea.png

了解了slice的底层结构, 我们看两个例子的不同之处,在于初始化slice时的容量。线程安全的例子中, x := []string{"start"}  的容量为1,在append操作时,会自动分配新的内存空间,故不存在数据竞争关系。 如下图:

00f7d81c938e08f668cc67424d2f388c.png

线程不安全的例子中, x := make([]string, 0, 6) 的容量为6。这里执行append操作时,Go注意到有空闲空间可以存放“Hello”, “World”等新的元素,而另一个协程也注意到有空间可以存放“Java”, “Go”,“React”等新的元素, 这时两个协程同时试图往同一块空闲空间中写入数据,竞争就出现了。最终谁胜出也就存在不确定性。如下图:

1611b8890f0858f5891e34115093dd6e.png

这是append的一个特点,而非bug。当每次调用append操作时,不用每次都关注是否需要分配新的内存。 优势是,允许用户在循环内追加,而无需破坏垃圾回收。缺点是,开发者必须意识到,当多个goroutine中的同一个原始切片被操作时,会存在线程不安全风险。

03

解决方案

最简单的解决方法是不使用多个切片操作同一个数组,以防止读写冲突。相反, 创建一个具有 所需总容量的新切片 ,并将新切片用作要追加的第一个变量。
package mainimport (  "fmt"  "sync")func main() {    x := make([]string, 0, 6)    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", "!")        fmt.Printf("y slice len:%d, cap:%d, value:%+v\n", len(y), cap(y), y)    }()        go func() {        defer wg.Done()        z := make([]string, 0, len(x) + 2) //分配新的内存        z = append(z, x...)        z = append(z, "PHP", "Go", "Java")        fmt.Printf("z slice len:%d, cap:%d, value:%+v\n", len(z), cap(z), z)    }()    wg.Wait()}

04

切片扩容基本规则

这里引用《Go专家编程》里面的基本扩容原则

1、 如果原slice的容量小于1024,则新slie的容量将扩大为原来的2倍 

2 、如果原slice的容量大于或等于1024,则新slice的容量将扩大为原来的1.25倍 

在该规则的基础上,还会考虑元素类型与内存分配规则,对实际扩张值做一些微调。从这个规则中可以看出Go对slice的性能和空间使用率的思考。 

1、当切片较小时,采用较大的扩容倍速,可以避免频繁地扩容,从而减少内存分配的2、次数和数据拷贝的代价 当切片较大时,采用较小的扩容倍速,主要是为了避免浪费空间。

Go专家编程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值