【Golang】golang并发模型
Golang调度器
先看看golang调度的由来。
一. 单进程时代不需要调度器
在早期操作系统是单进程的,一个进程拥有整个系统的所有资源,所以也不需要调度器。
但是单进程的操作系统也有明显的缺点:
1. 采用单一的执行流程,计算机只能一个任务一个任务处理。
2. 进程阻塞所造成CPU资源的浪费。
进程比较重,占用资源比较大,比较占用内存空间,CPU切换必然对性能有很大的影响。
相对于进程,线程虽然比进程轻量,也被称为轻量级进程(Lightweight Process,LWP),但是实际上多线程开发设计会变得更加复杂,要考虑很多同步竞争等问题,如锁、竞争冲突等,开发也变得非常复杂。
那么怎么才能提高CPU的利用率呢?
在Go语言中,协程(coroutine)是Go语言中的轻量级线程
实现,那么我们可以内核线程依然叫 “线程 (thread)”,而用户线程叫 “协程 (co-routine)”,如下。
上图里一个协程 (co-routine) 可以绑定一个线程 (thread),这种是一个协程对应一个线程,所以这种是1:1模型。协程的创建,删除,切换也都是需要CPU去完成的。
当然多个协程 (co-routine) 也可以绑定一个或者多个线程 (thread),这样就是N个协程对应一个线程 。
上图三个协程在用户态,对应内核空间的一个线程,用户态重的协程在工作时候也是可以进行切换的,但是这种切换是在用户态,协程之间的切换是由图中协程调度器去完成的,这样带来的好处就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。
但是N:1这种模型也是有缺点的
N:1模型的缺点:
- 某个程序用不了硬件的多核加速能力。
- 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。
进一步改良M:N模型,就是M个协程对应N个线程,如下图所示。
上图内核态有两个线程,当有两个CPU的时候,就可以利用多核,每个CPU可以绑定一个线程,不过这种处理模式就更加的复杂了,线程和协程之间都是通过协程调度器去协作和管理,所以调度器的性能就显得非常重要。
四. Go语言中的协程goroutine
在Go语言中,协程被称为goroutine,它非常轻量,一个goroutine只占几KB,这个比线程轻量一个数量级,因为占用内存小,所以调度更灵活 (runtime 调度),切换也可以很频繁。无论协程还是线程都需要调度器去完成调度,我们需要了解最关键的调度协程的调度器的实现原理。
来看看早期Go语言的goroutine调度器如何实现的。
在早期goroutine调度器中,每创建一个协程goroutine就会被添加到一个全局go协程队列中,当线程M0想要获取一个goroutine时候,就去从全局go协程队列中获取,全局go协程队列会有一个锁来保护,当线程获取锁之后就从全局队列中拿到一个goroutine并去执行,执行后就去将锁换回去,并将goroutine放回,这就是一个完整的过程。
这种调度器比较简单,但是也是有缺点的
缺点:
1. 创建,销毁,调度goroutine需要每个线程都先获得锁,这样就容易形成锁竞争。
2. 线程转移goroutine会造成延迟和额外的系统负载。比如线程(M1)当执行中的goroutine(G1)中包含创建新协程的时候,线程(M1)创建新的一个goroutine(G2),为了继续执行这个新的goroutine(G2),需要把这个新的goroutine(G2)交给另一个线程(M2)执行,也造成了很差的局部性,因为 (G2)和(G1)是相关的,最好放在(M1)上执行,而不是其他(M2)。
3. 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
CSP模型介绍
CSP模型的全称是Communicating Sequential Process,翻译是通信顺序进程,是一种并发编程模型,还有一种并发模型是Actor模式,著名的并发编程语言Erlang就是用的Actor模式。csp模型在上个世纪70年代就提出来了,是用于描述两个独立的并发实体通过共享的通讯channel(也就是管道)来进行通信的并发模型。相对于Actor模型来说,CSP中的channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时候所使用的channel,CSP模型是一种很强大的并发通讯模型,也成为面向并发编程语言的理论源头,也就诞生出后来的golang等语言。
对于golang来说,其实只用到了CSP模型的很小一部分,即Process/Channel,对应golang语言就是goroutine/channel,这两个并发关键字之间没有从属关系,Process可以是一个进程,线程,甚至可以是一个代码块,Process可以订阅任意个Channel,Channel也可不关心是从那个Process在利用它通信,Process围绕Channel进行读写。
goroutine 是通过 GMP 调度模型实现的。
G: 表示一个 goroutine,它有自己的栈。
M: 表示内核级线程,一个 M 就是一个线程,goroutine 跑在 M 之上的。
P: 全称是 Processor,处理器。它主要用来执行 goroutine 的,同时它也维护了一个 goroutine 队列。
参考来源
https://www.bilibili.com/read/cv5098443
https://morsmachine.dk/go-scheduler