Go语言的并发和并行

不知道你有没有注意到一个现象,还是这段代码,如果我跑在两个goroutines里面的话:

  1. var quit chan int = make(chan int)
  2.  
  3. func loop() {
  4. for i := 0; i < 10; i++ {
  5. fmt.Printf("%d ", i)
  6. }
  7. quit <- 0
  8. }
  9.  
  10. func main() {
  11. // 开两个goroutine跑函数loop, loop函数负责打印10个数
  12. go loop()
  13. go loop()
  14.  
  15. for i := 0; i < 2; i++ {
  16. <- quit
  17. }
  18. }

我们观察下输出:


  1. 这是不是有什么问题??

以前我们用线程去做类似任务的时候,系统的线程会抢占式地输出, 表现出来的是乱序地输出。而goroutine为什么是这样输出的呢?

goroutine是在并行吗?

我们找个例子测试下:

  1. package main
  2.  
  3. import "fmt"
  4. import "time"
  5.  
  6. var quit chan int
  7.  
  8. func foo(id int) {
  9. fmt.Println(id)
  10. time.Sleep(time.Second) // 停顿一秒
  11. quit <- 0 // 发消息:我执行完啦!
  12. }
  13.  
  14. func main() {
  15. count := 1000
  16. quit = make(chan int, count) // 缓冲1000个数据
  17.  
  18. for i := 0; i < count; i++ { //开1000个goroutine
  19. go foo(i)
  20. }
  21.  
  22. for i :=0 ; i < count; i++ { // 等待所有完成消息发送完毕。
  23. <- quit
  24. }
  25. }

让我们跑一下这个程序(之所以先编译再运行,是为了让程序跑的尽量快,测试结果更好):


  1. 我们看到,总计用时接近一秒。 貌似并行了!

我们需要首先考虑下什么是并发, 什么是并行

并行和并发

从概念上讲,并发和并行是不同的, 简单来说看这个图片(原图来自这里)

  • 两个队列,一个Coffee机器,那是并发
  • 两个队列,两个Coffee机器,那是并行

更多的资料: 并发不是并行, 当然Google上有更多关于并行和并发的区别。

那么回到一开始的疑问上,从上面的两个例子执行后的表现来看,多个goroutine跑loop函数会挨个goroutine去进行,而sleep则是一起执行的。

这是为什么?

默认地, Go所有的goroutines只能在一个线程里跑 。

也就是说, 以上两个代码都不是并行的,但是都是是并发的。

如果当前goroutine不发生阻塞,它是不会让出CPU给其他goroutine的, 所以例子一中的输出会是一个一个goroutine进行的,而sleep函数则阻塞掉了 当前goroutine, 当前goroutine主动让其他goroutine执行, 所以形成了逻辑上的并行, 也就是并发。

真正的并行

为了达到真正的并行,我们需要告诉Go我们允许同时最多使用多个核。

回到起初的例子,我们设置最大开2个原生线程, 我们需要用到runtime包(runtime包是goroutine的调度器):

  1. import (
  2. "fmt"
  3. "runtime"
  4. )
  5.  
  6. var quit chan int = make(chan int)
  7.  
  8. func loop() {
  9. for i := 0; i < 100; i++ { //为了观察,跑多些
  10. fmt.Printf("%d ", i)
  11. }
  12. quit <- 0
  13. }
  14.  
  15. func main() {
  16. runtime.GOMAXPROCS(2) // 最多使用2个核
  17.  
  18. go loop()
  19. go loop()
  20.  
  21. for i := 0; i < 2; i++ {
  22. <- quit
  23. }
  24. }

这下会看到两个goroutine会抢占式地输出数据了。

我们还可以这样显式地让出CPU时间:

  1. func loop() {
  2. for i := 0; i < 10; i++ {
  3. runtime.Gosched() // 显式地让出CPU时间给其他goroutine
  4. fmt.Printf("%d ", i)
  5. }
  6. quit <- 0
  7. }
  8.  
  9. func main() {
  10.  
  11. go loop()
  12. go loop()
  13.  
  14. for i := 0; i < 2; i++ {
  15. <- quit
  16. }
  17. }

观察下结果会看到这样有规律的输出:


  1. 其实,这种主动让出CPU时间的方式仍然是在单核里跑。但手工地切换goroutine导致了看上去的“并行”。

其实作为一个Python程序员,goroutine让我更多地想到的是gevent的协程,而不是原生线程。

关于runtime包对goroutine的调度,在stackoverflow上有一个不错的答案:http://stackoverflow.com/questions/13107958/what-exactly-does-runtime-gosched-do

一个小问题

我在Segmentfault看到了这个问题: http://segmentfault.com/q/1010000000207474

题目说,如下的程序,按照理解应该打印下5次 

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func say(s string) {
  6. for i := 0; i < 5; i++ {
  7. fmt.Println(s)
  8. }
  9. }
  10. func main() {
  11. go say("world") //开一个新的Goroutines执行
  12. for {
  13. }
  14. }

楼下的答案已经很棒了,这里Go仍然在使用单核,for死循环占据了单核CPU所有的资源,而main线和say两个goroutine都在一个线程里面, 所以say没有机会执行。解决方案还是两个:

  • 允许Go使用多核(手动显式调动( runtime调度器

    runtime调度器是个很神奇的东西,但是我真是但愿它不存在,我希望显式调度能更为自然些,多核处理默认开启。

    关于runtime包几个函数:

    • Gosched 让出cpu

    • NumCPU 返回当前系统的CPU核数量

    • 可同时使用的CPU核数

    • Goexit 退出当前goroutine(但是defer语句会照常执行)

    总结

    我们从例子中可以看到,默认的, 所有goroutine会在一个原生线程里跑,也就是只使用了一个CPU核。

    在同一个原生线程里,如果当前goroutine不发生阻塞,它是不会让出CPU时间给其他同线程的goroutines的,这是Go运行时对goroutine的调度,我们也可以使用runtime包来手工调度。

    本文开头的两个例子都是限制在单核CPU里执行的,所有的goroutines跑在一个线程里面,分析如下:

    • 对于代码例子一(loop函数的那个),每个goroutine没有发生堵塞(直到quit流入数据), 所以在quit之前每个goroutine不会主动让出CPU,也就发生了串行打印
    • 对于代码例子二(time的那个),每个goroutine在sleep被调用的时候会阻塞,让出CPU, 所以例子二并发执行。

    那么关于我们开启多核的时候呢?Go语言对goroutine的调度行为又是怎么样的?

    我们可以在Golang官方网站的这里 找到一句话:

    When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked.

    也就是说:

    当一个goroutine发生阻塞,Go会自动地把与该goroutine处于同一系统线程的其他goroutines转移到另一个系统线程上去,以使这些goroutines不阻塞

    开启多核的实验

    仍然需要做一个实验,来测试下多核支持下goroutines的对原生线程的分配, 也验证下我们所得到的结论“goroutine不阻塞不放开CPU”。

    实验代码如下:

    1. package main
    2.  
    3. import (
    4. "fmt"
    5. "runtime"
    6. )
    7.  
    8. var quit chan int = make(chan int)
    9.  
    10. func loop(id int) { // id: 该goroutine的标号
    11. for i := 0; i < 10; i++ { //打印10次该goroutine的标号
    12. fmt.Printf("%d ", id)
    13. }
    14. quit <- 0
    15. }
    16.  
    17. func main() {
    18. runtime.GOMAXPROCS(2) // 最多同时使用2个核
    19.  
    20. for i := 0; i < 3; i++ { //开三个goroutine
    21. go loop(i)
    22. }
    23.  
    24. for i := 0; i < 3; i++ {
    25. <- quit
    26. }
    27. }

    多跑几次会看到类似这些输出(不同机器环境不一样):


    1. 执行它我们会发现以下现象:
    • 有时会发生抢占式输出(说明Go开了不止一个原生线程,达到了真正的并行)
    • 有时会顺序输出, 打印完0再打印1, 再打印2(说明Go开一个原生线程,单线程上的goroutine不阻塞不松开CPU)

    那么,我们还会观察到一个现象,无论是抢占地输出还是顺序的输出,都会有那么两个数字表现出这样的现象:

    • 一个数字的所有输出都会在另一个数字的所有输出之前

    原因是, 3个goroutine分配到至多2个线程上,就会至少两个goroutine分配到同一个线程里,单线程里的goroutine 不阻塞不放开CPU, 也就发生了顺序输出。

go并发和并行的更多相关文章

  1. geotrellis使用(六)Scala并发(并行)编程

    本文主要讲解Scala的并发(并行)编程,那么为什么题目概称geotrellis使用(六)呢,主要因为本系列讲解如何使用Geotrellis,具体前几篇博文已经介绍过了.我觉得干任何一件事情基础很重要 ...

  2. Python 多线程教程:并发与并行

    转载于: https://my.oschina.net/leejun2005/blog/398826 在批评Python的讨论中,常常说起Python多线程是多么的难用.还有人对 global int ...

  3. java核心知识点学习----并发和并行的区别,进程和线程的区别,如何创建线程和线程的四种状态,什么是线程计时器

    多线程并发就像是内功,框架都像是外功,内功不足,外功也难得精要. 1.进程和线程的区别 一个程序至少有一个进程,一个进程至少有一个线程. 用工厂来比喻就是,一个工厂可以生产不同种类的产品,操作系统就是 ...

  4. Go语言并发与并行学习笔记(三)

    转:http://blog.csdn.net/kjfcpua/article/details/18265475 Go语言并发的设计模式和应用场景 以下设计模式和应用场景来自Google IO上的关于G ...

  5. Go语言并发与并行学习笔记(二)

    转:http://blog.csdn.net/kjfcpua/article/details/18265461 Go语言的并发和并行 不知道你有没有注意到一个现象,还是这段代码,如果我跑在两个goro ...

  6. Python并发与并行的新手指南

    点这里 在批评Python的讨论中,常常说起Python多线程是多么的难用.还有人对 global interpreter lock(也被亲切的称为“GIL”)指指点点,说它阻碍了Python的多线程 ...

  7. [CSAPP]并发与并行

    学了这么久的计算机,并发与并行的概念理解的一直不够透彻.考研复习那会儿,以为自己懂了,然而直到看了CSAPP才算是真正明白了这俩个概念. 并发(concurrency) 流X和流Y并发运行是指,流X在 ...

  8. 《Go in action》读后记录:Go的并发与并行

    本文的主要内容是: 了解goroutine,使用它来运行程序 了解Go是如何检测并修正竞争状态的(解决资源互斥访问的方式) 了解并使用通道chan来同步goroutine 一.使用goroutine来 ...

  9. 并发与并行的区别 The differences between Concurrency and Parallel

    逻辑控制流 在程序加载到内存并执行的时候(进程),操作系统会通过让它和其他进程分时段占用CPU(CPU slices)让它产生自己独占CPU的假象(同时通过虚拟内存让它产生独占内存的假象).在CPU在 ...

随机推荐

  1. Windows Phone 五、配置存储

    基本存储形式 本地设置:ApplicationData.Current.LocalSettings 漫游设置:ApplicationData.Current.RoamingSettings 支持的数据 ...

  2. NDK开发—基础知识实战Demo

    简介 前面写了几篇NDK相关的文章: NDK开发-简介&环境搭建(Eclipse,Android Studio) NDK开发-Android Studio+gradle-experimenta ...

  3. 关于Java 里的String和对象

    之前老师在课堂上讲过关于Java的引用,但是很遗憾,木有认真听啊,所以就在学习Java的过程中迷惑迷惑...最近好像明白一点Java的引用是怎么回事了.以下仅是我个人的理解,如果不对,还请不吝赐教. ...

  4. 论文阅读(Xiang Bai——【CVPR2015】Symmetry-Based Text Line Detection in Natural Scenes)

    Xiang Bai--[CVPR2015]Symmetry-Based Text Line Detection in Natural Scenes 目录 作者和相关链接 方法概括 创新点和贡献 方法细 ...

  5. windows registry => control pannel

    User Variables HKEY_CURRENT_USER\Environment System Variables HKEY_LOCAL_MACHINE\SYSTEM\CurrentContr ...

  6. JSON对象长度和遍历方法

    摘自博客 (http://caibaojian.com/json-length.html) 原文链接:http://caibaojian.com/json-length.html JSON数组有长度j ...

  7. Hdu 5489 合肥网络赛 1009 Removed Interval

    跳跃式LIS(nlogn),在普通的转移基础上增加一种可以跨越一段距离的转移,用一颗新的树状数组维护,同时,我们还要维护跨越完一次后面的转移,所以我用了3颗树状数组.. 比赛的时候一句话位置写错了,然 ...

  8. VS2010 MFC实现启动画面

    1.创建一个SDI工程(或者其他工程都可以). 2.创建一个位图资源命名为IDB_BITMAP1. 3.利用类向导添加一个类CMySplashCtrl(名字可以自己随便取). 4.在MySplashC ...

  9. greenDAO3 基本使用

    greenDAO3基本使用 greenDAO3开始使用注解的方式定义实体类(entity),并且是通过安装gradle插件来生成代码.之前的版本则是通过建立一个独立的Java-lib工程来存放生成的文 ...

  10. [转]iOS学习笔记(2)--Xcode6.1创建仅xib文件无storyboard的hello world应用

    转载地址:http://www.mamicode.com/info-detail-514151.html 由于Xcode6之后,默认创建storyboard而非xib文件,而作为初学,了解xib的加载 ...