一、前言
在文章的开头,请大家来跟我一起看一段代码:
代码的意思是,我开启一万个协程,向切片中写入每一次遍历的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
41package 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
进行并发编程的时候,会不会产生这个问题?下一篇文章进行揭晓~