设为首页 收藏本站
查看: 1480|回复: 0

[经验分享] golang的并发不等于并行

[复制链接]

尚未签到

发表于 2018-9-19 12:12:41 | 显示全部楼层 |阅读模式
  转自个人博客 chinazt.cc
  先看下面一道面试题:
  

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>  

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>  
}

  
fmt.Println("go routine 1 i: ", i,>  
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>  
}

  
fmt.Println("go routine 2 i: ", i,>  
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官方已经发过预警提示。 只管自己看官方文档的习惯,所以直接栽坑里了。
  好在及时发现了自己的不足,亡羊补牢,为时未晚吧。



运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-594250-1-1.html 上篇帖子: golang slice 切片原理 下篇帖子: golang
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表