• 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

    这是不是有什么问题??

    以前我们用线程去做类似任务的时候,系统的线程会抢占式地输出, 表现出来的是乱序地输出。而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
        }
    }

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

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

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

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

    并行和并发

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

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

    那么回到一开始的疑问上,从上面的两个例子执行后的表现来看,多个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
        }
    }

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

    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, 所以例子二并发执行。
  • 相关阅读:
    UVALive 7352 Dance Recital
    [ An Ac a Day ^_^ ] UVALive 7270 Osu! Master
    vim配置文件
    数据结构 链表
    [ An Ac a Day ^_^ ] hrbust 2291 Help C5 分形
    [ An Ac a Day ^_^ ] hdu 2553 N皇后问题 搜索
    [ An Ac a Day ^_^ ] HihoCoder 1249 Xiongnu's Land 线性扫描
    hdu 5874 Friends and Enemies icpc大连站网络赛 1007 数学
    hdu 5876 Sparse Graph icpc大连站网络赛 1009 补图最短路
    6.Z字变换 direction
  • 原文地址:https://www.cnblogs.com/liuzhongchao/p/9528155.html
Copyright © 2020-2023  润新知