Golang goroutine

作者: swapmem | 来源:发表于2018-10-06 20:48 被阅读19次

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 的关系入下图所示

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()

参考链接

相关文章

  • go学习笔记(四)——并发

    1. goroutine 什么是goroutine goroutine是golang的最小执行单元,每个go程序至...

  • go

    https://golang.google.cn/ goroutine简单示例 goroutine将结果通过cha...

  • goroutine 及channel

    本文主要介绍goroutine 和channel的使用。 基础知识介绍 goroutine 是 golang 中在...

  • golang channel 系统学习

    goroutine是个啥 使用golang的channel之前,我们需要先了解go的goroutine。Go 语言...

  • Golang goroutine

    goroutine 是 Golang的最大卖点之一,它让并发编程变的十分简单,仅仅使用 go关键字就能快速的创建g...

  • Golang goroutine

    goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协...

  • golang goroutine

    其实用go也用了一段时间,我是看视频入门的,然后去买书,反正来来回回用了一年多的时间,很多点知道怎么用,相关知识也...

  • goroutine and channel 2020-07-20

    参考:https://draveness.me/golang/docs goroutine and channel...

  • golang实现并发

    要知道的是golang使用goroutine实现并发,goroutine只是官方实现的超级“线程池”,每个实例4-...

  • golang的垃圾回收(GC)机制

    请先阅读 golang的goroutine调度机制 golang的垃圾回收采用的是 标记-清理(Mark-and-...

网友评论

    本文标题:Golang goroutine

    本文链接:https://www.haomeiwen.com/subject/vxzgextx.html