小锟哥哥

V1

2022/07/04阅读:20主题:萌绿

n++也不靠谱

今天小明又去面试了,又被问了一个奇怪的面试题:

n := 0
for i := 0; i < 1000000; i++ {
 go func() {
  n++
 }()
}
fmt.Println(n)

到你思考的时间了,输出啥结果呢?

小明思考了许久,给出了他的回答:不知道,然后面试官就告诉他:你通过了。

是不是有点离谱,没错,这个代码的结果就是不知道,每次执行的结果都不一样,全看 cpu 咋调度。

且听我来给客官慢慢道来。

一、最开始的原型

我们根据面试代码,往回滚一点,看下这样的代码:

n := 0
for i := 0; i < 1000000; i++ {
 func() {
  n++
 }()
}
fmt.Println(n)

我们把协程拿掉,现在的结果是不是就很好知道了,没错就是循环的次数 1000000。

二、里面的坑

我们再回到面试的代码,这里面其实有两个坑:

第一个坑:他没加协程等待,所以很可能一扫而过,还没循环几次主程序就结束了,甚至是一次循环都没做就退出了。

但是在面试中,一般不提这个坑,这不是面试的重点,当然你也可以提一下。

第二个坑就是面试的重点了:

在不考虑主线程提前退出的问题,就是加入协程后,n++ 的结果不准确了。

为什么呢?

因为 n++ 并不是原子的,他要完成 n++ 的操作他需要做三步:

  • 从内存里面取出值
  • 执行 +1 操作
  • 赋值回去

因为他不是原子的,所以很可能在你取值的时候别的线层也在取值,也在进行计算,最后赋值时就会被覆盖,从而出现随机不可预算的结果。

三、该怎么保证结果呢?

因为 n++ 不是原子的,如果我们要让他变原子,常见的操作有两种:

1、加锁

首先我们为了保证他能把循环执行完毕,需要加个 wait:

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)

这样就能让他执行完毕了,再加入我们的线层锁:

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 包来处理这类操作,但是也有一定局限,他支持的数据类型有限。

直接上代码:

var n int32 = 0
for i := 0; i < 1000000; i++ {
 func() {
  atomic.AddInt32(&n, 1//原子操作
 }()
}
fmt.Println(n)

这里我们把 n 变成了 int32 类型,这样的运行结果也能保证是循环的次数。

分类:

后端

标签:

Golang

作者介绍

小锟哥哥
V1

公众号:GoLang全栈