go 源码篇(三)CSP GMP Channel

1goroutine原理

1.1基本概念

  1. 并发:
    一个CPU上能同时执行多项任务,在很短时间内,CPU来回切换任务执行(在某段很短时间内执行程序a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执 行),这样看起来多个任务像是同时执行,这就是并发。
  2. 并行
    当系统有多个CPU时,每个CPU同一时刻都运行任务,互不抢占自己所在的CPU资源,同时进行, 称为并行。
  3. 进程
    CPU在切换程序的时候,如果不保存上一个程序的状态(context–上下文),直接切换下一个程 序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需 要的资源。因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一 个实体)。
  4. 线程
    CPU切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需 要内核态都需要读取用户态的数据,进程一旦多起来,CPU调度会消耗一大堆资源,因此引入了 线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程 切换那么耗费资源。
  5. 协程:用户态轻量级线程
    1. 调度完全又用户控制,操作系统看不见协程同一时刻一个CPU只会执行一个协程.
    2. 轻量:
      1. 创建代价小:协程只需要很小很小的空间. 其他的东西都是多个协程共享的
      2. 协程切换只涉及基本的CPU上下文切换,所谓的 CPU 上下文,就是一堆寄存器
    3. 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即 所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说 法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。

1.2 Go并发模式

  1. 生产者消费者模型
  2. 发布订阅模型

1.2 Go并发模型

  1. Go实现了两种并发形式。
    1. 第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。
    2. 另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。

1.2.1CSP思想

  1. 请记住下面这句话:
    以通信的方式来共享内存,不要以共享内存的方式来通信,相反,要通过通信来共享内存。
  2. 为什么要以通信的方式来共享内存?
    普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”,加锁的本身就是一种问题

1.2.2CSP实现

  1. Go的CSP并发模型,是通过goroutine和channel来实现的。
    1. goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似, 可以理解为”线程“。
    2. channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个 goroutine之间通信的”管道“,有点类似于Linux中的管道。

1.3Go调度器GMP

1.3.1GMP

M指的是Machine,一个M直接关联了一个内核线程。
P指的是"processor",代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。什么是上下文环境,我的理解, 肯定有go 函数运行起来的所需要的资源,
G指的是Goroutine,其实本质上也是一种轻量级的线程

在这里插入图片描述

1.3.2调度流程

  1. 当一个Goroutine创建被创建时,Goroutine对象被压入Processor的本地队列或者Go运行时全局Goroutine队列。
  2. Processor唤醒一个Machine,如果Machine的waiting队列没有等待被唤醒的Machine, 则创建一个(只要不超过Machine的最大值,10000),Processor获取到Machine后,与此Machine绑定,并执行此Goroutine。
  3. Machine执行过程中,随时会发生上下文切换。当发生上下文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器中Machine的栈保存在Goroutine对 象上,只需要将Machine所需要的寄存器(堆栈指针、程序计数器等)保存到Goroutine对象上 即可。
  4. 如果此时Goroutine任务还没有执行完,Machine可以将Goroutine重新压入Processor的队 列,等待下一次被调度执行。
  5. 如果执行过程遇到阻塞并阻塞超时,Machine会与Processor分离,并等待阻塞结束。此时 Processor可以继续唤醒Machine执行其它的Goroutine,当阻塞结束时,Machine会尝试” 偷取”一个Processor,如果失败,这个Goroutine会被加入到全局队列中,然后Machine将 自己转入Waiting队列,等待被再次唤醒。
    在这里插入图片描述

1.3.3问题

你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的,当遇到内核线程阻塞的时候,我们可以直接放开其他线程,

2channel 原理

2.1channel数据结构

在这里插入图片描述
在这里插入图片描述

2.2channel实现方式

chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个元素的channel示意图:

  1. dataqsiz指示了队列长度为6,即可缓存6个元素。
  2. buf指向队列的内存,队列中还剩余两个元素。
  3. qcount表示队列中还有两个元素。
  4. sendx指示后续写入的数据存储的位置,取值[0, 6]。
  5. recvx指示从该位置读取数据, 取值[0, 6]。

2.3向channel写数据

向一个channel中写数据简单过程如下:

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

2.4从channel读数据

从一个channel读数据简单过程如下:

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

2.5关闭channel

关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤 醒,但这些G会panic。

2.6panic出现的常见场景还有:

  1. 关闭值为nil的channel
  2. 关闭已经被关闭的channel
  3. 向已经关闭的channel写数据
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值