go并发和并行

Go语言的并发和并行

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

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

咱们观察下输出:python

这是否是有什么问题??

 

之前咱们用线程去作相似任务的时候,系统的线程会抢占式地输出, 表现出来的是乱序地输出。而goroutine为何是这样输出的呢?git

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

让咱们跑一下这个程序(之因此先编译再运行,是为了让程序跑的尽可能快,测试结果更好):github

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

 

咱们须要首先考虑下什么是并发, 什么是并行golang

并行和并发

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

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

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

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

这是为何?oop

默认地, 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, 也就发生了顺序输出。

相关文章
相关标签/搜索