文章目录
1goroutine原理
1.1基本概念
- 并发:
一个CPU上能同时执行多项任务,在很短时间内,CPU来回切换任务执行(在某段很短时间内执行程序a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执 行),这样看起来多个任务像是同时执行,这就是并发。- 并行
当系统有多个CPU时,每个CPU同一时刻都运行任务,互不抢占自己所在的CPU资源,同时进行, 称为并行。- 进程
CPU在切换程序的时候,如果不保存上一个程序的状态(context–上下文),直接切换下一个程 序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需 要的资源。因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一 个实体)。- 线程
CPU切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度需 要内核态都需要读取用户态的数据,进程一旦多起来,CPU调度会消耗一大堆资源,因此引入了 线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程 切换那么耗费资源。- 协程:用户态轻量级线程
- 调度完全又用户控制,操作系统看不见协程同一时刻一个CPU只会执行一个协程.
- 轻量:
- 创建代价小:协程只需要很小很小的空间. 其他的东西都是多个协程共享的
- 协程切换只涉及基本的CPU上下文切换,所谓的 CPU 上下文,就是一堆寄存器
- 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即 所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说 法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。
1.2 Go并发模式
- 生产者消费者模型
- 发布订阅模型
1.2 Go并发模型
- Go实现了两种并发形式。
- 第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。
- 另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。
1.2.1CSP思想
- 请记住下面这句话:
以通信的方式来共享内存,不要以共享内存的方式来通信,相反,要通过通信来共享内存。- 为什么要以通信的方式来共享内存?
普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”,加锁的本身就是一种问题
1.2.2CSP实现
- Go的CSP并发模型,是通过goroutine和channel来实现的。
- goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似, 可以理解为”线程“。
- channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个 goroutine之间通信的”管道“,有点类似于Linux中的管道。
1.3Go调度器GMP
1.3.1GMP
M指的是Machine,一个M直接关联了一个内核线程。
P指的是"processor",代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。什么是上下文环境,我的理解, 肯定有go 函数运行起来的所需要的资源,
G指的是Goroutine,其实本质上也是一种轻量级的线程
1.3.2调度流程
- 当一个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队列,等待被再次唤醒。
1.3.3问题
你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的,当遇到内核线程阻塞的时候,我们可以直接放开其他线程,
2channel 原理
2.1channel数据结构
2.2channel实现方式
chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个元素的channel示意图:
- dataqsiz指示了队列长度为6,即可缓存6个元素。
- buf指向队列的内存,队列中还剩余两个元素。
- qcount表示队列中还有两个元素。
- sendx指示后续写入的数据存储的位置,取值[0, 6]。
- recvx指示从该位置读取数据, 取值[0, 6]。
2.3向channel写数据
向一个channel中写数据简单过程如下:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq 取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被
读goroutine唤醒;
简单流程图如下:
img
2.4从channel读数据
从一个channel读数据简单过程如下:
- 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出, 最后把G唤醒,结束读取过程;
- 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中 数据写入缓冲区尾部,把G唤醒,结束读取过程;
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
- 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;
2.5关闭channel
关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤 醒,但这些G会panic。
2.6panic出现的常见场景还有:
- 关闭值为nil的channel
- 关闭已经被关闭的channel
- 向已经关闭的channel写数据