问题引入
我们先看下下面的代码,看看输出的结果是什么,符合我们的预期吗?
func main() {
counter := 0
for i := 0; i < 5000; i++ {
go func() {
counter++
}()
}
time.Sleep(time.Second * 1)
fmt.Println("counter = ", counter)
}
运行过上面代码的同学应该都发现了:
- 每次的输出结果都是不一样的
- 每次的结果都不是5000
造成上述问题的原因就是该程序不是线程安全的,每个协程拿到的counter的值是不一样的,可能有些协程快些,counter的值就大,有些则不然
解决
这里我们可以尝试着用锁来处理
func TestCounterThreadSafe(t *testing.T) {
var mut sync.Mutex
counter := 0
for i := 0; i < 5000; i++ {
go func() {
defer func() {
mut.Unlock()
}()
mut.Lock()
counter++
}()
}
time.Sleep(time.Second * 1)
fmt.Println("counter = ", counter)
}
这里我们引入锁的概念,大家可以理解成,现在每个协程都在抢着要counter变量,就相当于每个协程要去上厕所,目前只有一个厕所,每个协程谁先抢到了厕所,就在厕所上上锁,那么别的协程想要上厕所就只能在外面等着,等抢到counter的协程处理完后再用counter做处理
分析一
我们上面的代码可能有些同学就发现了,为什么还有加上一个time.Sleep()呢?好,那我们去掉这个等待看看结果会怎么样?
func TestCounterThreadSafe(t *testing.T) {
var mut sync.Mutex
counter := 0
for i := 0; i < 5000; i++ {
go func() {
defer func() {
mut.Unlock()
}()
mut.Lock()
counter++
}()
}
fmt.Println("counter = ", counter)
}
大家可以看到,结果又和我们的预期出现了错乱。
原因呢就是,main函数是一个主协程,其他由go修饰的函数是子协程,当主协程结束退出后,子协程也会跟着死亡,无论子协程的代码是否执行完。所以我们在前面加上1秒的等待时间是给子携程争取时间的
分析二
因为我们不知道这5000个协程执行完成后到底会花多长时间,所以我们自己决定就等1秒,但这样多少是不严谨的。 假如我们有一个这样的场景,当所有的5000个协程执行总共要花1分钟,但是我们的主协程就等1秒中,这种情况会导致协程都还没执行完程序就中断了 为了处理这种情况,换句话说,我们不想手动计算所有协程的总耗时,go语言给我们提供了这样一个方法:只有等所有被注册的协程执行完后,才能执行后续的操作
func TestCounterThreadSafe(t *testing.T) {
var mut sync.Mutex
var wg sync.WaitGroup
counter := 0
for i := 0; i < 5000; i++ {
wg.Add(1) // 注册一个协程
go func() {
defer func() {
mut.Unlock()
}()
mut.Lock()
counter++
wg.Done() // 协程完成
}()
}
wg.Wait() // 等待所有注册的协程完成后在执行后续操作
fmt.Println("counter = ", counter)
}
写在最后
好,到现在为止,协程安全的最终实现就是这样,锁处理数据安全,wg处理防止主协程退出 大家一定要注意,锁和wg解决的问题是不一样的