Golang——groutine和channel底层机制

goroutine, channel其实可以认为是golang中runtime的一部分,当然golang的runtime还包含Garbage collection,内存分配,垃圾回收,interfaces, maps,slices等一些高级功能

并发

我们知道Golang系统默认支持原始并发(加入协程与管道),不想其他语言一样,需要写一堆的代码,还没有几个能写好的。

Golang使用Groutine和channels实现了CSP(Communicating Sequential Processes)模型,channles负责goroutine通信。

goroutinue

goroutine是Golang实现的用户空间的轻量级的线程,有runtime调度器调度,与操作系统的thread有多对一的关系

  • goroutinue,本质上就是协程。但有两点不同:
    • 1.goroutinue可以实现并行,多个协程可以在多个处理器同时跑。而协程同一时刻只能在一个处理器上跑(把宿主语言想象成单线程的就好了)。
    • 2.goroutine之间的通信是通过channel,而协程的通信是通过yield和resume()操作。

goroutine scheduler

goroutine scheduler 是Go runtime的一个重要的组成部分。他负责追踪,调度每个goroutine运行,实际上是从应用程序的process所属的thread pool中分配一个thread来执行这个goroutine。因此,和java虚拟机中的Java thread和OS thread映射概念类似,每个goroutine只有分配到一个OS thread才能运行。

Chanel

Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)

初始化

  • ch := make(chan Task, 3) // hchan(src/runtime/chan.go)

创建channel时在该进程的heap申请一块内存,创建一个hchan结构体,返回执行该内存的指针(ch变量本身是一个指针,在函数间传递的时候是同一个channel)

channel使用一个唤醒队列保存groutine之间传递的数据,使用两个list保存goroutine(向该chan发生数据,从该chan接受数据),还有一个mutex保证操作安全

模型

M是操作系统的线程,G是用户启动的goroutine,P是与调度相关的context,每个M都拥有一个P,P维护了一个能够运行的goutine队列,用于该线程执行。

发生接受数据

向channel发送和从channel接收数据主要涉及hchan里的四个成员变量

buf(指向dataqsiz元素的数组),sendx,recvx,lock(锁定保护HCHA中的所有字段)
  • 初始化的时候hchan中buf为空,sendx,recvx为0:
  • 向chan发生数据的时候,会对buf加锁,然后将要发生的数据copy到buf中,并sendx+1,最后释放buf的锁。
  • 从chan接受数据的时候,会对buf加锁,然后将buf里面的数据copy到变量对应的内存,并recvx+1,最后释放buf的锁。

底层通过hchan中的buf,使用copy内存的方式通讯,达到共享内存目的

阻塞

  • 当想已满的chan发生数据,runtime检测到对应的hchan已经满了,会通知调度器,调度器将发送至置为waiting,移除与线程M的关系,
    然后从P的runqueue中选择一个goroutine在线程M中执行,此时发送者处于阻塞状态,但是操作系统线程非阻塞,所以只消耗少量资源。
  • 发送者阻塞后会创建一个自己的结构体sudog,然后放到sendq(发送阻塞列表:保存channel相关变量的指针,如发送或者接收数据的变量的地址&copy)
  • 从chan接收数据时,会通知调度器,将发送者状态设置为runnable,并且将其加入P的runqueue,等待线程执行

注意:如果接受者先运行,那么他会从一个空的chanl中取数据,这个时候会直接阻塞,通发送者阻塞一样,也会创建一个自己的结构体sudog,保存接收数据的变量的地址,
但是该sudog不是放在recvq(接收阻塞列表),当再想chan发送数据的时候,runtime辟谷没有对hchan中buf加锁,而是直接将发送的数据copy到接收者的结构体sudog对应的elem指向的内存地址

Runtime

goroutine, channel其实可以认为是golang中runtime的一部分,当然golang的runtime还包含Garbage collection,内存分配,垃圾回收,interfaces, maps,slices等一些高级功能

1.4之前,runtime还是由C语言所编写的,官方计划,1.5版本将去除C的代码,runtime将完全由Go语言来完成,不论何种方式,runtime和用户编译后的代码被linker静态链接起来,形成一个可执行文件。这个文件从操作系统角度来说是一个user space的独立的可执行文件。

从运行的角度来说,这个文件由2部分组成,一部分是用户的代码,另一部分就是runtime。runtime通过接口函数调用来管理goroutine, channel及其他一些高级的功能。从用户代码发起的调用操作系统API的调用都会被runtime拦截并处理。

总结:

goroutine的调度器是在线程之上的多路复用。channel的实现是仅仅的关联在调度器之上,compiler也是紧密的和goroutine调度器关联在一起,不仅仅创建goroutine,而且也管理着stack,防止stack溢出。goroutine是一个执行的stack+一个控制的struct。调度器分配goroutine在线程上执行,当一个线程阻塞了或者调用一个非Go的函数(CGO调用),那么调度器就会开始一个新的线程来运行其他的goroutine.

坚持原创技术分享,您的支持将鼓励我继续创作!