Dreamer2q Blog
见到,不如不见
Dreamer2q

Code is cheap, talk is expensive

64日志

Goroutine 与 线程

创建于 2021-02-15 共 1280 字,阅读约 5 分钟 更新于 21-02-15 13:52
浏览 11评论 0

可增长的栈


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


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


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


  • 使用通道构造一个把任意多个goroutine串联再一起的流水程序。在内存耗尽之前你能创建的最大流水线级数是多少?一个值穿过整个流水线需要多久?


func main(){

    var ch = make(chan struct{})
    var number int = 1e6
    fmt.Println("Creating goroutines",number)
    startTime := time.Now()
    for i := 0; i < number; i++ {
        go func(in <-chan struct{}, out chan<- struct{}) {  //流水线
            out <- <-in
        }(ch, ch)
    }
    fmt.Println("Creating finished",time.Since(startTime))
    startTime = time.Now()
    ch <- struct{}{}
    <- ch
    fmt.Println("Time: ", time.Since(startTime))
}


Output


Creating goroutines 1000000
Creating finished 6.6581723s
Time:  11.6568727s


当我尝试创建3*10^6个goroutine时,我的goland卡没了,就是自动关闭了。

下图是我重开的goland,然后时间看不到了。

image


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 15.8037546s
Time:  405.9402ms
//GOMAXPROCS = 2
Creating goroutines 1000000
Creating finished 6.8087977s
Time:  10.2795211s

//GOMAXPROCS = 1
Counter: 3264829
//GOMAXPROCS = 2
Counter: 1572741


嘤嘤?


个人理解


  • 对于练习1
    当只有一个线程的时候,goroutine的调度都是Go自己的调度器来实现的,因此成本比多个线程,靠OS来调度的成本低。导致,多个线程的时候性能反而没有一个线程好。但是创建goroutine的时候多线程就发挥了优势,但是更多线程的时候可能受制于内存的速度,导致创建goroutine的一个瓶颈。
  • 对于练习2
    还是受制于OS调度开销的影响。这里开了两个goroutine但是两个不能同时运行,其中有一个处于阻塞状态,这样的话,如果开两个线程,去处理一个goroutine确实有种帮倒忙的感觉。


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


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


goroutine 没有标识


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


结尾


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


嘤嘤嘤


goroutine好用就对了。