我们都知道,多个线程操作同一个变量,是有线程安全问题的。但是,如果换成是“多个协程操作同一个变量”呢?还会有安全问题吗?
Windows 11
Go 1.20.2
先看一段Golang代码示例:
func main() {
count := 0
wg := sync.WaitGroup{}
wg.Add(2)
// 协程1
go func() {
for i := 0; i < 100000; i++ {
count++
}
wg.Done()
}()
// 协程2
go func() {
for i := 0; i < 100000; i++ {
count++
}
wg.Done()
}()
wg.Wait() // 等待上面两个协程都执行完了,再往下执行
fmt.Println(count)
}
上面代码中,协程1 和 协程2 共同对 count
变量执行 +1 操作,各自循环10万遍。
理论上最后输出count
的值应该等于20万,但实际输出的值远小于20万。比如我的输出结果是116346
为什么会这样呢,首先是因为 count++
不是一个原子性的操作,它实际是由三句代码组成的,等同于:
tmp := count
tmp = tmp + 1
count = tmp
是一个“先查询后更新”的操作。
然后,Go语言默认情况下是多线程的,线程数量默认等于CPU的核心数。比如双核CPU,Go就会开启两个线程来运行协程,协程1
在线程A
上执行,协程2
在线程B
上执行,由于是多核CPU,这两个线程是可以并行执行的。因此归根到底,这实际上是一个线程安全的问题,即多个线程操作同一个变量导致的。
既然是多线程的原因,那如果改成单线程,是不是就不会有问题了呢?
Go语言刚好有提供这样的配置:runtime.GOMAXPROCS(N)
,其中N
就是你想要设置的线程数量。想改为单线程那么只要将N
设置为 1 就好。
更改后的代码例子:
func main() {
runtime.GOMAXPROCS(1) // 设置为单线程
count := 0
wg := sync.WaitGroup{}
wg.Add(2)
// 协程1
go func() {
for i := 0; i < 100000; i++ {
count++
}
wg.Done()
}()
// 协程2
go func() {
for i := 0; i < 100000; i++ {
count++
}
wg.Done()
}()
wg.Wait() // 等待上面两个协程都执行完了,再往下执行
fmt.Println(count) // 输出:200000
}
果然改为单线程后,就不存在“线程安全”的问题了,能正确输出count
的值了。
注:在实际生产环境中,不建议通过设置为单线程模式来避免此类问题,因为单线程会降低Go执行协程的效率,可以通过其它方式,比如加锁、channel之类的方式来解决。
改为单线程后,就可以毫无顾虑的使用多协程了吗?答:并不是。
在某些场景下如果不注意还是会有隐患的,比如这段代码:
func main() {
runtime.GOMAXPROCS(1) // 设置为单线程
count := 0
wg := sync.WaitGroup{}
wg.Add(2)
// 协程1
go func() {
for i := 0; i < 100000; i++ {
count++
}
wg.Done()
}()
// 协程2
go func() {
fmt.Printf("我是协程2,此时count = %d\n", count)
for i := 0; i < 100000; i++ {
tmp := count
tmp = tmp + 1
count = tmp
}
time.Sleep(time.Second * 2) // 此处会迫使协程2让出执行权
fmt.Printf("我是协程2,已经执行了10万次 +1 操作,此时count = %d\n", count)
wg.Done()
}()
wg.Wait() // 等待上面两个协程都执行完了,再往下执行
}
上述代码会输出:
我是协程2,此时count = 0
我是协程2,已经执行了10万次 +1 操作,此时count = 200000
协程2
一开始查询到的count
值是0,执行了10万次+1后,count
值居然变成了20万。
这是因为睡眠语句time.Sleep(time.Second * 2)
会迫使协程2
让出CPU的使用权,让出CPU使用权后,当前线程会改为去执行协程1
,当协程1
执行完后或遇到 IO阻塞 时,才会又切回来执行协程2
,但切回来后,此时的count
值已经被协程1
修改过了,所以肯定跟协程2
刚开始时查询到的值是不一样的。
除了sleep语句外,当协程遇到IO阻塞时,也会让出CPU使用权
所以在协程执行过程中,如果我们想要确保全局变量不会被其它协程修改,就要给变量加锁。
https://zhuanlan.zhihu.com/p/40279108
https://segmentfault.com/a/1190000041568839