goroutine原理的图文详解

Go并发模型

	Don’t communicate by sharing memory; share memory by communicating.
	不要以共享内存的⽅式来通信,要通过通信来共享内存。

Go调度器GMP

Go语⾔运⾏时环境提供了⾮常强⼤的管理goroutine和系统内核线程的调度器, 内部提供了三种
对象: Goroutine, Machine, Processor。

  • Goroutine : 指应⽤创建的goroutine
  • Machine : 指系统内核线程。
  • Processor : 指承载多个goroutine的运⾏器

在宏观上说, Goroutine与Machine因为Processor的存在,形成了多对多(M:N)的关系。 M个
⽤户线程对应N个系统线程,缺点增加了调度器的实现难度。

Goroutine是Go语⾔中并发的执⾏单位。 Goroutine底层是使⽤协程(coroutine)实现, coroutine
是⼀种运⾏在⽤户态的⽤户线程(参考操作系统原理:内核态,⽤户态)它可以由语⾔和框架层
调度。 Go在语⾔层⾯实现了调度器,同时对⽹络, IO库进⾏了封装处理,屏蔽了操作系统层⾯的
复杂的细节,在语⾔层⾯提供统⼀的关键字⽀持。

三者与内核级线程的关系如下图所示:
goroutine原理的图文详解_第1张图片

  • ⼀个Machine会对应⼀个内核线程(K),同时会有⼀个Processor与它绑定。⼀个Processor连接
    ⼀个或者多个Goroutine。 Processor有⼀个运⾏时的Goroutine(上图中绿⾊的G),其它的
    Goroutine处于等待状态。

  • Processor的数量同时可以并发任务的数量,可通过GOMAXPROCS限制同时执⾏⽤户级任务的操
    作系统线程。 GOMAXPROCS值默认是CPU的可⽤核⼼数,但是其数量是可以指定的。

  • 当⼀个Goroutine创建被创建时, Goroutine对象被压⼊Processor的本地队列或者Go运⾏时
    全局Goroutine队列。

  • Processor唤醒⼀个Machine,如果Machine的waiting队列没有等待被 唤醒的Machine,则
    创建⼀个(只要不超过Machine的最⼤值, 10000), Processor获取到Machine后,与此
    Machine绑定,并执⾏此Goroutine。

  • Machine执⾏过程中,随时会发⽣上下⽂切换。当发⽣上下⽂切换时,需要对执⾏现场进⾏
    保护,以便下次被调度执⾏时进⾏现场恢复。 Go调度器中Machine的栈保存在Goroutine对
    象上,只需要将Machine所需要的寄存器(堆栈指针、程序计数器等)保存到Goroutine对象上
    即可。

  • 如果此时Goroutine任务还没有执⾏完, Machine可以将Goroutine重新压⼊Processor的队
    列,等待下⼀次被调度执⾏。

  • 如果执⾏过程遇到阻塞并阻塞超时, Machine会与Processor分离,并等待阻塞结束。此时
    Processor可以继续唤醒Machine执⾏其它的Goroutine,当阻塞结束时, Machine会尝
    试”偷取”⼀个Processor,如果失败,这个Goroutine会被加⼊到全局队列中,然后Machine
    将⾃⼰转⼊Waiting队列,等待被再次唤醒。

channel数据结构

Go依赖于称为CSP(Communicating Sequential Processes)的并发模型,通过
Channel实现这种同步模式。

  • channel结构体:
//path: src/runtime/chan.go
type hchan struct {
qcount uint // 当前队列中剩余元素个数
dataqsiz uint // 环形队列⻓度,即可以存放的元素个数
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素的⼤⼩
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 队列下标,指示元素写⼊时存放到队列中的位置
recvx uint // 队列下标,指示元素从队列的该位置读出
recvq waitq // 等待读消息的goroutine队列
sendq waitq // 等待写消息的goroutine队列
lock mutex // 互斥锁, chan不允许并发读写
}
  • 向channel写数据

      向⼀个channel中写数据简单过程如下:
      1. 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写⼊,最后把该G唤醒,结束发送过程;
      2. 如果缓冲区中有空余位置,将数据写⼊缓冲区,结束发送过程;
      3. 如果缓冲区中没有空余位置,将待发送数据写⼊G,将当前G加⼊sendq,进⼊睡眠,等待被读goroutine唤醒;
    

    goroutine原理的图文详解_第2张图片

  • 从channel读数据

      从⼀个channel读数据简单过程如下:
      1. 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
      2. 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中⾸部读出数据,把G中数据写⼊缓冲区尾部,把G唤醒,结束读取过程;
      3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
      4. 将当前goroutine加⼊recvq,进⼊睡眠,等待被写goroutine唤醒;
    

goroutine原理的图文详解_第3张图片

  • 关闭channel

      关闭channel时会把recvq中的G全部唤醒,本该写⼊G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic。
      
      
      除此之外, panic出现的常⻅场景还有:
      
      1. 关闭值为nil的channel
      2. 关闭已经被关闭的channel
      3. 向已经关闭的channel写数据
    

你可能感兴趣的:(Go)