Goroutine 与 线程

可增长的栈

每个OS线程有一个固定大小的栈内存(通常2MB),栈内存用于保存函数调用中的局部变量。

对于一个小的goroutine,比如仅仅等待一个WaitGroup再关闭一个通道,2MB的栈太浪费了。对于复杂和深度递归函数,固定大小的栈不够大。

一个goroutine生命周期开始时只有一个很小的栈(通常2KB),与OS线程不同的是,goroutine的栈不是大小固定的,可以按需增大和缩小。

  • 使用通道构造一个把任意多个goroutine串联再一起的流水程序。在内存耗尽之前你能创建的最大流水线级数是多少?一个值穿过整个流水线需要多久?
func main() {
    var ch = make(chan int)
    var number int = 1e6
    fmt.Println("Creating goroutines",number)
    startTime := time.Now()
    var n = sync.WaitGroup{}
    for i := 0; i <= number; i++ {
        n.Add(1)
        go func(in <-chan int, out chan<- int,n *sync.WaitGroup) {
            out <- <-in
            n.Done()
        }(ch, ch,&n)
    }
    n.Add(-1)
    fmt.Println("Creating finished",time.Since(startTime))
    startTime = time.Now()
    ch <- 1
    n.Wait()
    fmt.Println("Time: ", time.Since(startTime))
}

Output

Creating goroutines 1000000
Creating finished 6.6581723s
Time:  11.6568727s

当我尝试创建3*10^6个goroutine时,我的goland卡没了,就是自动关闭了。
下图是我重开的goland,然后时间看不到了。
3*10^6个goroutine

goroutine真是强大,可以随便开到10W个。

电脑配置

  • i5-8265U
  • 8g ddr4 2666

goroutine调度

OS线程由OS内核来调度。因为OS线程由内核来调度,所有控制权权限从一个线程到另一个线程需要一个完整的上下文切换(context switch)。

Go运行时包含一个自己的调度器,这个调度器使用一个称为m:n调度的技术,它可以复用/调度m个goroutine到n个OS线程。

与操作系统线程调度器不同,Go调度器由特定的Go语言结构来触发。因为它不需要切换到内核语境,所以调用一个goroutine比调用一个线程成本低很多。

  • 写一个程序,两个goroutine通过两个无缓冲通道来互相转发消息。这个程序能每秒多少次通信?
func main() {
    var counter int64
    var t = time.NewTimer(1 * time.Second)
    var chIn = make(chan struct{})
    var chOut = make(chan struct{})
    var done = make(chan struct{})
    go func() {
        for {
            select {
            case <-done:
                return
            default:
                chIn <- struct{}{}
                <-chOut
                counter++
            }
        }
    }()
    go func() {
        for {
            select {
            case <-done:
                return
            default:
                <-chIn
                chOut <- struct{}{}
            }
        }
    }()
    select {
    case <-t.C:
        close(done)
        fmt.Println("Counter:",counter)
    }
}

Output

Counter: 1816315

GOMAXPROCS

Go调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。
默认是机器的CPU内核数(我们有超线程啊,一个掰成俩来用!?)。

正在休眠或者正在被通道通信阻塞的goroutine不需要占用线程。阻塞在I/O和其它系统调中或调用非Go语言写的函数的goroutine需要一个独立的OS线程,但这个线程不计算在GOMAXPROCS

  • 那么问题来了,前面的两个小练习都是基于默认的GOMAXPROCS运行的,如果修改GOMAXPROCS会发生什么?
//GOMAXPROCS = 1
Creating goroutines 1000000
Creating finished 5.5462063s
Time:  9.256258s
//GOMAXPROCS = 4
Creating goroutines 1000000
Creating finished 5.7835657s
Time:  3.1585553s

//GOMAXPROCS = 1
Counter: 3264829
//GOMAXPROCS = 4
Counter: 1736502

嘤嘤?

个人理解

  • 对于练习1
    我沮丧的发现,仅仅增加线程的数量,不能保证性能一定会得到提升。可能是因为开了太多的goroutine,因此收到其它环境的影响就会比较大,但是对于许多个goroutine的情况下,开多个线程往往会取得比较好的结果。具体的瓶颈在哪里我没法去深度了解,也不知道。
  • 对于练习2
    受制于OS调度开销的影响。这里开了两个goroutine但是两个不能同时运行,其中有一个处于阻塞状态,这样的话,如果开多个个线程,去处理一个goroutine确实有种帮倒忙的感觉。

这两个例子就是给我们理解goroutine协程的轻量吧。

!!!此外goroutine调度的因素很多,运行时也在不断变化,这里的结果真的有时候差距很大。!!!

goroutine 没有标识

这个看的不是很懂。。。。呜呜...

结尾

goroutine线程的差距本质上是属于量变,但一个足够大的量会变成质变。

嘤嘤嘤

goroutine好用就对了!

Last modification:January 22nd, 2020 at 11:51 pm
要饭啦~