一、前言
在文章的开头,请大家来跟我一起看一段代码:
代码的意思是,我开启一万个协程,向切片中写入每一次遍历的key,心里思考一下最后输出这个切片的长度是多少?
| 1 | func Test_concurrency_append_slice(t *testing.T) { | 
相信看完之后,如果有过并发编程经验的小伙伴应该知道结果肯定不等于一万,这是为什么呢?我在 常规php 里循环一万次向数组里丢东西,数组长度一定是一万,这里怎么就不同了呢?
这里其实就涉及到了线程安全问题,且听我慢慢道来~
二、线程安全问题
先解疑,这里 php 可以是因为 常规php 是串行,而 go 开启协程是并行向切片写入数据,slice 是对数组一个连续片段的引用,当 slice 长度增加的时候,可能底层的数组会被换掉。问题出在换底层数组之前,切片的指针同时被多个 goroutine 拿到,并执行 append 操作。那么很多 goroutine 的 append 结果会被覆盖,导致n个 goroutine append后,长度小于n。
如何解决这个问题呢?这里有两种方法去解决
- 加互斥锁 
 具体代码如下:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41- package tests 
 import (
 "fmt"
 "log"
 "sync"
 "testing"
 )
 func Test_concurrency_append_slice(t *testing.T) {
 var count = 10000
 var wg = sync.WaitGroup{}
 var ch = make(chan struct{}, count)
 //互斥锁
 var lock = sync.Mutex{}
 var countSlice []int
 for i := 0; i <= count; i++ {
 wg.Add(1)
 ch <- struct{}{}
 go func(count int) {
 if r := recover(); r != nil {
 log.Fatalf("func has panic: %v", r)
 }
 defer func() {
 wg.Done()
 <-ch
 lock.Unlock()
 }()
 lock.Lock()
 countSlice = append(countSlice, count)
 }(i)
 }
 wg.Wait()
 fmt.Println(len(countSlice))
 }- 这样就可以保证每次在 - append的时候只有一个- goroutine能写入,其他的- goroutine都在等待,不会产生并发写入争抢内存地址的问题,但是这样只能临时解决问题,对于性能要求不高的业务可以使用,但是如果业务对于性能要求很高,那么这种等待的方案就显得有些不足了~
- channel 串行化 
 channel是什么:- Channel是- Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯
| 1 | package tests | 
由代码中可以看出,我们使用了 channel 在多个协程中接收变量,最后在协程外再将 channel 中的数据添加到 slice 中,这样我们就不会因为并发的对切片写入争抢地址而发愁了
三、结语
为什么我要分享这个呢?我觉得这是最容易出错的地方之一,对于懂的人,觉得这个真的很简单,但是对于一些其他语言转到 go 这个语言来的开发者,尤其是 php 开发者,超级容易犯这个错,这对于不仔细观察数据的童鞋可能是致命的,甚至会产生生产事故,所以我超喜欢我在寻找分享话题的时候(不定期举行内部专业线会议,需要分享东西),同事和我说的一句话:”也许你觉得很简单的东西,恰巧是别人最想听的”。那么话说回来了,php 就真的没有这个问题么?我们在使用 swoole 进行并发编程的时候,会不会产生这个问题?下一篇文章进行揭晓~