继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Golang goroutine

幕布斯6054654
关注TA
已关注
手记 1135
粉丝 218
获赞 1009

goroutine 是 Golang的最大卖点之一,它让并发编程变的十分简单,仅仅使用 go关键字就能快速的创建goroutine。与其他语言设计并发程序相比,这极大的减少了程序员的心智负担。

goroutine的特点

  • 轻量级

goroutine是用户态"线程",开销非常小,最新golang版本默认为goroutine分配的初始栈大小为2k,同时会根据运行状况动态扩展或收缩。一个有2G内存的机器,理论上可以容纳一百万 goroutine。

  • 协作式调度

golang的runtime采用协作式调度,goroutine的运行原则上不能被抢占,除非goroutine主动让出CPU,否则goroutine会运行到结束,所以context switch 开销基本可以忽略。

  • 高效的线程模型

golang为了充分发挥多核机器的优势,采用了M:N线程模型,即M个内核线程,每个内核线程可以为N个goroutine提供运行环境,最大限度的发挥了多核机器的能力。

几个关键的数据结构

  • g

g代表一个goroutine实例,在golang源码src/runtime/runtime2.go 中,可以看到g的详细定义。和普通的线程一样,g主要包含:可伸缩的运行栈,goroutine切换时的上下文环境(gobuf),程序计数器,基地址,可执行代码等。

type g struct {
        stack      stack   // offset known to runtime/cgo
        sched     gobuf
        goid        int64
        gopc       uintptr // pc of go statement that created this goroutine
        startpc    uintptr // pc of goroutine function
        ... ...
}
  • m

m代表一个内核线程,是goroutine真正的执行环境。一般会有一个内核线程池,当goroutine因为等待网络数据或者读取文件等阻塞时,goroutine会绑定在这个m上,等到阻塞操作的完成后重新绑定到一个p上继续运行。若暂时找不到可用的p,那么这个goroutine会放到全局的 run queue 中。

type m struct {
    g0      *g     // goroutine with scheduling stack
    mstartfn      func()
    curg          *g       // current running goroutine
 .... ..
}
  • p

早起版本的golang实现不包含p这一结构,p表示一个逻辑处理器,p的数量一般为机器的CPU核心数,每个p下面挂载有等待被调度的goroutine. 每个 goroutine想要运行需要首先获得p才能被调度。p数量决定了系统的最大并发度。

type p struct {
    lock mutex

    id          int32
    status      uint32 // one of pidle/prunning/...

    mcache      *mcache
    racectx     uintptr

    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32
    runqtail uint32
    runq     [256]guintptr

    runnext guintptr    // Available G's (status == Gdead)
    gfree    *g
    gfreecnt int32

  ... ...
}

g, m, p 的关系如下图所示

webp

G,M,P关系图(图片来自网络)

上图左半部分,M1为空闲线程,M0线程下面有一个P和它绑定,P下面有一个正在运行的G0,还有其他等待运行的G。在某个时候,G0中发生了系统调用,P与M0解绑,寻找空闲的线程M1,绑定到上面继续执行P下的其他G,M0与G0陷入系统调用,如上图右半部分所示。

为何需要抢占式调度

goroutine里面的代码执行没有确定的时间,如果一个goroutine长期占有p运行,甚至一个死循环,那么p下面的其他g就无法得到调度,这种情况是我们不希望看到的。幸好,系统监控线程 sysmon可以判断这种情况,它可以打断当前goroutine的执行,使P下的其他G得到调度。

sysmon主要完成如下工作:

  • 释放闲置超过5分钟的span物理内存;

  • 如果超过2分钟没有垃圾回收,强制执行;

  • 将长时间未处理的netpoll结果添加到任务队列;

  • 向长时间运行的G任务发出抢占调度;

  • 收回因syscall长时间阻塞的P;

因此,我们不应该在goroutine里面设计长时间运行的任务。这种抢占机制在一定程度上保证了同一P下G的公平调度。

work stealing 算法

当p下面没有可供调度的goroutine时,他会从global run queue或者其他p下的goroutine中“偷” 一部分goroutine来运行,这样最大限度的利用多核。这在一定程度上保证了在各个CPU核上的负载均衡。

如何处理阻塞的系统调用

对于普通的文件IO操作一旦阻塞,那么m就会进入sleep状态,IO完成之后才会被唤醒。这种情况下,p将与m分离,选择其他空闲的m继续执行。如果没有空闲的m,那么就会新创建一个m。可想而知,如果有大量的这样的文件IO操作,大量的m将会被创建出来,这时候操作系统对m的调度开销就不能忽视了。

针对网络IO,golang使用netpoller做出了特别的优化,这样goroutine里面发起网络IO也不会导致m被阻塞,从而不会引起创建大量的内核线程m。

goroutine发生调度的时机

goroutine在获得m时一般不能一直运行到完毕,它们往往可能要等待其他资源才能执行完成,比如说一个http请求收到服务器响应这个goroutine才算完成了他的任务。在等待服务器响应的这一段时间它不会占用CPU时间 ,调度器会调度其他goroutine继续执行。goroutine遇到下面的情况下可能会产生重新调度

  • 阻塞 I/O

  • select操作

  • 阻塞在channel

  • 等待锁

  • 主动调用 runtime.Gosched()



作者:江湖兵
链接:https://www.jianshu.com/p/fc35e081d900


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP