golang的并发不等于并行
转自个人博客 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 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官方已经发过预警提示。 只管自己看官方文档的习惯,所以直接栽坑里了。
好在及时发现了自己的不足,亡羊补牢,为时未晚吧。
golang的并发不等于并行的更多相关文章
- golang实现并发爬虫三(用队列调度器实现)
欲看此文,必先可先看: golang实现并发爬虫一(单任务版本爬虫功能) gollang实现并发爬虫二(简单调度器) 上文中的用简单的调度器实现了并发爬虫. 并且,也提到了这种并发爬虫的实现可以提高爬 ...
- golang的并发
Golang的并发涉及二个概念: goroutine channel goroutine由关键字go创建. channel由关键字chan定义 channel的理解稍难点, 最简单地, 你把它当成Un ...
- 并发(Concurrency)和并行(Parallelism)的区别
最近在读<real world haskell>里关于并行的一章时,看到作者首先对并发(Concurrency)和并行(Parallelism)的区别进行了定义和解释.以前我对这个问题也是 ...
- Java进阶7 并发优化2 并行程序设计模式
Java进阶7 并发优化2 并行程序设计模式20131114 1.Master-worker模式 前面讲解了Future模式,并且使用了简单的FutureTask来实现并发中的Future模式.下面介 ...
- go---weichart个人对Golang中并发理解
个人觉得goroutine是Go并行设计的核心,goroutine是协程,但比线程占用更少.golang对并发的处理采用了协程的技术.golang的goroutine就是协程的实现. 十几个gorou ...
- golang语言并发与并行——goroutine和channel的详细理解(一)
如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...
- golang语言并发与并行——goroutine和channel的详细理解
如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...
- golang语言并发与并行——goroutine和channel的详细理解(一) 转发自https://blog.csdn.net/skh2015java/article/details/60330785
如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据. ...
- 【GoLang】并发小结
006.并发 1 概念 1.1 goroutine是Go并行设计的核心,goroutine的本质是轻量级线程 1.2 golang的runtime实现了对轻量级线程即goroutine的智能调度管理 ...
随机推荐
- PHP 底层的运行机制与原理 --转
发现一片总结的还不错的文章,记录一下 PHP说简单,但是要精通也不是一件简单的事.我们除了会使用之外,还得知道它底层的工作原理. PHP是一种适用于web开发的动态语言.具体点说,就是一个用C语言实现 ...
- Java中线程总结
本文简要介绍在 Java 世界中, 线程相关知识.主要包含 线程的创建与销毁:线程安全与同步:线程通讯:注意本文没有什么高深新知识,只缘起前段时间在翻看项目代码的时候,发现有些同学对此有诸多误解,故在 ...
- 项目管理之 SVN 管理软件 CornerStone for Mac
常用的项目管理有 Git 和 SVN.之前公司一直使用的是 Git,使用的是 SourceTree 客户端,据说 Git 比 SVN 要好,只能说各有特点吧,有兴趣的可以查看下两个的区别. 下面是学习 ...
- DedeCMS数据负载性能优化方案简单几招让你提速N倍
前文介绍了DedeCMS栏目列表页实现完美分页的方法,避免了大部分重复栏目标题对搜索引擎的影响,对SEO更有利.今天,分享一下DedeCMS数据负载性能优化的方法. 接触织梦也有三年多时间了,对它可谓 ...
- 地理位置 API
js获取地理位置的接口navigator.geolocation geolocation对象有三个方法 1.getCurrentPosition 2.watchPosition 3.clearWatc ...
- Zepto源码分析-deferred模块
源码注释 // Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT l ...
- php curl 的几个实例
使用PHP的cURL库可以简单和有效地去抓网页.你只需要运行一个脚本,然后分析一下你所抓取的网页,然后就可以以程序的方式得到你想要的数据了.无论是你想从从一个链接上取部分数据,或是取一个XML文件并把 ...
- [OpenGL]配置GLFW
注:本文可转载,转载请著名出处:http://www.cnblogs.com/collectionne/p/6937644.html.本文还会修改,如果不在博客园(cnblogs)发现本文,建议访问上 ...
- cpp(第六章)
1. #include <iostream> #include <limits> int main() { ; ) { std::cout<<"enter ...
- 【MyBatis源码分析】select源码分析及小结
示例代码 之前的文章说过,对于MyBatis来说insert.update.delete是一组的,因为对于MyBatis来说它们都是update:select是一组的,因为对于MyBatis来说它就是 ...