Golang 并发Groutine实例解读(一)

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

咱们观察下输出:函数

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

这是否是有什么问题??oop

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

goroutine是在并行吗?

咱们找个例子测试下:ui

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

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

go build test.go
time ./test
./test  0.01s user 0.01s system 1% cpu 1.016 total

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

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

并行和并发

从概念上讲,并发和并行是不一样的, 简单来讲看这个图片code

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

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

这是为何?

默认地, 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
    }
}

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

0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9

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

runtime调度器

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

关于runtime包几个函数:

  • Gosched 让出cpu

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

  • GOMAXPROCS 设置最大的可同时使用的CPU核数

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

总结

咱们从例子中能够看到,默认的, 全部goroutine会在一个原生线程里跑,也就是只使用了一个CPU核。

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

 

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

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