前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >n++也不靠谱

n++也不靠谱

作者头像
小锟哥哥
发布2022-12-05 14:02:34
2520
发布2022-12-05 14:02:34
举报
文章被收录于专栏:GoLang全栈

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

代码语言: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 类型,这样的运行结果也能保证是循环的次数。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-07-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GoLang全栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、最开始的原型
  • 二、里面的坑
  • 三、该怎么保证结果呢?
    • 1、加锁
      • 2、使用 atomic
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档