分享
  1. 首页
  2. 文章

golang的并发不等于并行

Andy · · 3203 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

先 看下面一道面试题:

func main() { 
runtime.GOMAXPROCS(1) 
wg := sync.WaitGroup{} 
wg.Add(20) 
for i := 0; i < 10; i++ { 
go func() { 
fmt.Println("go routine 1 i: ", i) 
wg.Done() 
}()
}
for i := 0; i < 10; i++ { 
go func(i int) { 
fmt.Println("go routine 2 i: ", i) 
wg.Done() 
}(i)
}
wg.Wait() 
}

在不执行代码的前提下,脑补一下输出结果应该是什么。

我再看到这道题时,首先想到输出应该是0 -- 9 依次输出。 但执行后才大跌眼镜,错的不是一点半点。首先看一下,在我本地执行的结果:

go routine 2 i: 9 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 1 i: 10 
go routine 2 i: 0 
go routine 2 i: 1 
go routine 2 i: 2 
go routine 2 i: 3 
go routine 2 i: 4 
go routine 2 i: 5 
go routine 2 i: 6 
go routine 2 i: 7 
go routine 2 i: 8 

意不意外? 惊不惊喜?

为什么会是这样的结果, 再翻阅了google官方出品的golang文档之后,总算搞到了一些头绪。

并发不等于并行

golang的核心开发人员Rob Pike专门提到了这个话题(有兴趣可以看这个视频或者看原文PPT)

虽然我们在for循环中使用了go 创建了一个goroutine,我们想当然会认为,每次循环变量时,golang一定会执行这个goroutine,然后输出当时的变量。 这时,我们就陷入了思维定势。 默认并发等于并行。

诚然,通过go创建的goroutine是会并发的执行其中的函数代码。 但一定会按照我们所设想的那样每次循环时执行吗? 答案是否定的!

Rob Pike专门提到了golang中并发指的是代码结构中的某些函数逻辑上可以同时运行,但物理上未必会同时运行。而并行则指的就是在物理层面也就是使用了不同CPU在执行不同或者相同的任务。

golang的goroutine调度模型决定了,每个goroutine是运行在虚拟CPU中的(也就是我们通过runtime.GOMAXPROCS(1)所设定的虚拟CPU个数)。 虚拟CPU个数未必会和实际CPU个数相吻合。每个goroutine都会被一个特定的P(虚拟CPU)选定维护,而M(物理计算资源)每次回挑选一个有效P,然后执行P中的goroutine。

每个P会将自己所维护的goroutine放到一个G队列中,其中就包括了goroutine堆栈信息,是否可执行信息等等。默认情况下,P的数量与实际物理CPU的数量相等。因此当我们通过循环来创建goroutine时,每个goroutine会被分配到不同的P队列中。而M的数量又不是唯一的,当M随机挑选P时,也就等同随机挑选了goroutine。

在本题中,我们设定了P=1。所以所有的goroutine会被绑定到同一个P中。 如果我们修改runtime.GOMAXPROCS的值,就会看到另外的顺序。 如果我们输出goroutine id,就可以看到随机挑选的效果:

func main() { 
wg := sync.WaitGroup{} 
wg.Add(20) 
for i := 0; i < 10; i++ { 
go func() { 
var buf [64]byte 
n := runtime.Stack(buf[:], false) 
idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] 
id, err := strconv.Atoi(idField) 
if err != nil { 
panic(fmt.Sprintf("cannot get goroutine id: %v", err)) 
}
fmt.Println("go routine 1 i: ", i, id) 
wg.Done() 
}()
}
for i := 0; i < 10; i++ { 
go func(i int) { 
var buf [64]byte 
n := runtime.Stack(buf[:], false) 
idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] 
id, err := strconv.Atoi(idField) 
if err != nil { 
panic(fmt.Sprintf("cannot get goroutine id: %v", err)) 
}
fmt.Println("go routine 2 i: ", i, id) 
wg.Done() 
}(i)
}
wg.Wait() 
}

输出如下:

go routine 2 i: 9 24 
go routine 1 i: 10 11 
go routine 1 i: 10 5 
go routine 1 i: 10 6 
go routine 2 i: 3 18 
go routine 1 i: 10 9 
go routine 1 i: 10 10 
go routine 1 i: 10 8 
go routine 2 i: 0 15 
go routine 2 i: 4 19 
go routine 2 i: 6 21 
go routine 1 i: 10 7 
go routine 1 i: 10 14 
go routine 2 i: 7 22 
go routine 2 i: 8 23 
go routine 1 i: 10 13 
go routine 2 i: 5 20 
go routine 1 i: 10 12 
go routine 2 i: 1 16 
go routine 2 i: 2 17
⋊> ~/S/g/g/s/t/C/goroutine ./goroutine
go routine 1 i: 10 11 
go routine 2 i: 9 24 
go routine 1 i: 10 6 
go routine 1 i: 10 14 
go routine 1 i: 10 9 
go routine 1 i: 10 10 
go routine 1 i: 10 12 
go routine 2 i: 0 15 
go routine 1 i: 10 13 
go routine 1 i: 10 5 
go routine 2 i: 1 16 
go routine 2 i: 5 20 
go routine 1 i: 10 7 
go routine 2 i: 7 22 
go routine 2 i: 3 18 
go routine 2 i: 2 17 
go routine 2 i: 4 19 
go routine 1 i: 10 8 
go routine 2 i: 8 23 
go routine 2 i: 6 21 

我们再回到这道题中,虽然在循环中通过go定义了一个goroutine。但我们说到了,并发不等于并行。因此虽然定义了,但此刻不见得就会去执行。需要等待M选择P之后,才能去执行goroutine。 关于golang中goroutine是如何进行调度的(GPM模型),可以参考Scalable Go Scheduler Design Doc或者LearnConcurrency

这时应该就可以理解为什么会先输出goroutine2然后再输出goroutine1了吧。

下面我们来解释为什么goroutine1中输出的都是10.

goroutine如何绑定变量

在golang的for循环中,golang每次都使用相同的变量实例(也就是题中所使用的i)。 而golang之间是共享环境变量的。

当调度到这个goroutine时,它就直接读取所保存的变量地址,此时就会出现一个问题:goroutine保存的只是变量地址,所以变量是有可能被修改的

再结合题中的for循环,每次使用的都是同一个变量地址,也就是说i每次都在变化,到循环结束之时,i就变成了10. 而goroutine中保存的也只有i的内存地址而已,所以当goroutine1执行时,毫不犹豫的就把i的内容读了出来,多少呢? 10!

但为什么goroutine2不是10呢?

反过来看goroutine2,就容易理解了。因为在每次循环中都重新生成了一个新变量,然后每个goroutine保存的是各自新变量的地址。 这些变量相互之间互不干扰,不会被任何人所篡改。因此在输出时,会从0 - 9依次输出。

其实这些问题,golang官方已经发过预警提示。 只管自己看官方文档的习惯,所以直接栽坑里了。

好在及时发现了自己的不足,亡羊补牢,为时未晚吧。


有疑问加站长微信联系(非本文作者)

本文来自:Andy's Blog

感谢作者:Andy

查看原文:golang的并发不等于并行

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
3203 次点击
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏