Go语言的并发和并行

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

var quit chan int = make(chan int)

func loop() {
for i := 0; i < 10; i++ {
fmt.Printf("%d ", i)
}
quit <- 0
} func main() {
// 开两个goroutine跑函数loop, loop函数负责打印10个数
go loop()
go loop() for i := 0; i < 2; i++ {
<- quit
}
}

我们观察下输出:


这是不是有什么问题??

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

goroutine是在并行吗?

我们找个例子测试下:

package main

import "fmt"
import "time" var quit chan int func foo(id int) {
fmt.Println(id)
time.Sleep(time.Second) // 停顿一秒
quit <- 0 // 发消息:我执行完啦!
} func main() {
count := 1000
quit = make(chan int, count) // 缓冲1000个数据 for i := 0; i < count; i++ { //开1000个goroutine
go foo(i)
} for i :=0 ; i < count; i++ { // 等待所有完成消息发送完毕。
<- quit
}
}

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


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

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

并行和并发

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

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

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

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

这是为什么?

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

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

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

真正的并行

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

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

import (
"fmt"
"runtime"
) var quit chan int = make(chan int) func loop() {
for i := 0; i < 100; i++ { //为了观察,跑多些
fmt.Printf("%d ", i)
}
quit <- 0
} func main() {
runtime.GOMAXPROCS(2) // 最多使用2个核 go loop()
go loop() for i := 0; i < 2; i++ {
<- quit
}
}

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

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

func loop() {
for i := 0; i < 10; i++ {
runtime.Gosched() // 显式地让出CPU时间给其他goroutine
fmt.Printf("%d ", i)
}
quit <- 0
} func main() { go loop()
go loop() for i := 0; i < 2; i++ {
<- quit
}
}

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


其实,这种主动让出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次 

package main

import (
"fmt"
) func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
} func main() {
go say("world") //开一个新的Goroutines执行
for {
}
}

楼下的答案已经很棒了,这里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”。

    实验代码如下:

    package main
    
    import (
    "fmt"
    "runtime"
    ) var quit chan int = make(chan int) func loop(id int) { // id: 该goroutine的标号
    for i := 0; i < 10; i++ { //打印10次该goroutine的标号
    fmt.Printf("%d ", id)
    }
    quit <- 0
    } func main() {
    runtime.GOMAXPROCS(2) // 最多同时使用2个核 for i := 0; i < 3; i++ { //开三个goroutine
    go loop(i)
    } for i := 0; i < 3; i++ {
    <- quit
    }
    }

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


    执行它我们会发现以下现象:
    • 有时会发生抢占式输出(说明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. Mybatis传多个参数(三种解决方案)

    http://blog.csdn.net/liangyihuai/article/details/49965869 (zhuan)

  2. Qt之Qprocess

    QProcess 可用于完成启动外部程序,并与之交互通信. 一.启动外部程序的两种方式 1)一体式:void QProcess::start(const QString & program,c ...

  3. mybatis+MySQL--CRUD

    ①导入jar包: ②.配置config.xml: ③.entity: mapping: ④.DAO:   —————————————————————————————————— 目录结构: —————— ...

  4. MS CRM 2013 Plugin 注册工具登录后空白

    解决办法 把en-us, zh-CN 目录随便改个名字就好了

  5. [转]SSAS没有注册类 (异常来自 HRESULT:0x80040154 (REGDB_E_CLASSNOTREG)) (Microsoft Visual Studio)的解决办法

    转自:http://www.cnblogs.com/xvqm00/archive/2011/07/15/2107338.html 打开SSAS 数据源视图浏览数据时,提示 没有注册类别 (异常来自 H ...

  6. 瘋子C语言笔记(结构体/共用体/枚举篇)

    (一)结构体类型 1.简介: 例: struct date { int month; int day; int year; }; struct student { int num; char name ...

  7. SuiteScript > Script Queue Monitor (Beta)

    Share Note: Installing and Accessing the Script Queue Monitor Script Queue Monitor (Beta) is availab ...

  8. Android项目架构之业务组件化

    前言: 从个人经历来说的话,从事APP开发这么多年来,所接触的APP的体积变得越来越大,业务的也变得越来越复杂,总来来说只有一句话:这是一个APP臃肿的时代!所以为了告别APP臃肿的时代,让我们进入一 ...

  9. 复旦大学2015--2016学年第二学期(15级)高等代数II期末考试第六大题解答

    六.(本题10分)  设 $n$ 阶复方阵 $A$ 的特征多项式为 $f(\lambda)$, 复系数多项式 $g(\lambda)$ 满足 $(f(g(\lambda)),g'(\lambda))= ...

  10. MyBatis支持的jdbcType类型

    BIT         FLOAT      CHAR           TIMESTAMP       OTHER       UNDEFINED TINYINT     REAL       V ...