今天小明又去面试了,又被问了一个奇怪的面试题:
代码语言:javascript复制n := 0
for i := 0; i < 1000000; i {
go func() {
n
}()
}
fmt.Println(n)
到你思考的时间了,输出啥结果呢?
小明思考了许久,给出了他的回答:不知道,然后面试官就告诉他:你通过了。
是不是有点离谱,没错,这个代码的结果就是不知道,每次执行的结果都不一样,全看 cpu 咋调度。
且听我来给客官慢慢道来。
一、最开始的原型
我们根据面试代码,往回滚一点,看下这样的代码:
代码语言:javascript复制n := 0
for i := 0; i < 1000000; i {
func() {
n
}()
}
fmt.Println(n)
我们把协程拿掉,现在的结果是不是就很好知道了,没错就是循环的次数 1000000。
二、里面的坑
我们再回到面试的代码,这里面其实有两个坑:
第一个坑:他没加协程等待,所以很可能一扫而过,还没循环几次主程序就结束了,甚至是一次循环都没做就退出了。
但是在面试中,一般不提这个坑,这不是面试的重点,当然你也可以提一下。
第二个坑就是面试的重点了:
在不考虑主线程提前退出的问题,就是加入协程后,n 的结果不准确了。
为什么呢?
因为 n 并不是原子的,他要完成 n 的操作他需要做三步:
- 从内存里面取出值
- 执行 1 操作
- 赋值回去
因为他不是原子的,所以很可能在你取值的时候别的线层也在取值,也在进行计算,最后赋值时就会被覆盖,从而出现随机不可预算的结果。
三、该怎么保证结果呢?
因为 n 不是原子的,如果我们要让他变原子,常见的操作有两种:
1、加锁
首先我们为了保证他能把循环执行完毕,需要加个 wait:
代码语言:javascript复制wg := sync.WaitGroup{}
n := 0
for i := 0; i < 1000000; i {
wg.Add(1)
go func() {
defer wg.Done()
n //不是原子的 1、从内存读出 2、n 3、赋值
}()
}
wg.Wait()
fmt.Println(n)
这样就能让他执行完毕了,再加入我们的线层锁:
代码语言:javascript复制wg := sync.WaitGroup{}
locker := sync.Mutex{}
n := 0
for i := 0; i < 1000000; i {
wg.Add(1)
go func() {
defer wg.Done()
// 锁
defer locker.Unlock()
locker.Lock()
n //不是原子的 1、从内存读出 2、n 3、赋值
}()
}
wg.Wait()
fmt.Println(n)
这样执行的结果,每次都是执行的次数了。
2、使用 atomic
我们偶尔还会使用 atomic 包来处理这类操作,但是也有一定局限,他支持的数据类型有限。
直接上代码:
代码语言:javascript复制var n int32 = 0
for i := 0; i < 1000000; i {
func() {
atomic.AddInt32(&n, 1) //原子操作
}()
}
fmt.Println(n)
这里我们把 n 变成了 int32 类型,这样的运行结果也能保证是循环的次数。