概念
什么是并发❓
一个 CPU 上能同时执行多项任务,在很短时间内,CPU 来回切换任务执行(在某段很短时间内执行程序 a,然后又迅速得切换到程序b去执行),有时间上的重叠(宏观上是同时的,微观仍是顺序执行),这样看起来多个任务像是同时执行,这就是并发。
什么是并行❓
当系统有多个CPU时,每个CPU同一时刻都运行任务,互不抢占自己所在的CPU资源,同时进行,称为并行。
什么是进程❓
CPU在切换程序的时候,如果不保存上一个程序的状态(context—上下文),直接切换下一个程序,就会丢失上一个程序的一系列状态,于是引入了进程这个概念,用以划分好程序运行时所需要的资源。因此进程就是一个程序运行时候的所需要的基本资源单位(也可以说是程序运行的一个实体)。
什么是线程❓
CPU切换多个进程的时候,会花费不少的时间,因为切换进程需要切换到内核态,而每次调度内核态都需要读取用户态的数据,进程一旦多起来,CPU调度会消耗一大堆资源,因此引入了线程的概念,线程本身几乎不占有资源,他们共享进程里的资源,内核调度起来不会那么像进程切换那么耗费资源。
什么是协程❓
协程拥有自己的寄存器、上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。
调度器 - GMP
Go语言运行时环境提供了非常强大的管理goroutine和系统内核线程的调度器 - GMP(Goroutine Machine Processor)。
- Goroutine:指应用创建的 goroutine;
- Machine:指系统内核线程;
- Processor:指承载多个 goroutine 的运行器;
在宏观上说,Goroutine 与 Machine 因为 Processor 的存在而形成了多对多(M:N)的关系,即M个用户线程对应 N 个系统线程。缺点是增加了调度器的实现难度。
一个Machine对应一个内核线程(K),同时会有至少一个 Processor 与它绑定。一个 Processor 可连接一个或者多个 Goroutine。Processor 有且仅有一个正在运行的 Goroutine,其它的 Goroutine 处于等待状态。
Processor 的数量代表着可并发的任务数量,可通过 GOMAXPROCS 限制 Processor 个数(默认为CPU的可用核心数)。
runtime.GOMAXPROCS(MaxProcs) // 指定最大Processor数量
Goroutine
Go语言中,每一个并发执行的活动称为 Goroutine。Goroutine 底层是使用协程 (coroutine) 实现,coroutine 是一种运行在用户态的用户线程(参考操作系统原理:内核态,用户态)它可以由语言和框架层调度。Go 在语言层面实现了调度器,同时对网络,IO 库进行了封装处理,屏蔽了操作系统层面的复杂的细节,在语言层面提供统一的关键字 (go) 支持。
- 一个 G 被创建后,会被压入 P 的本地队列或 Go 运行时的全局队列中;
为防止队列中的其他 G 被饿死,一个 G 一次最多可执行 10ms,超过 10ms 后会被从新压入队尾等待下次执行;
Machine
系统内核态线程,默认最多可创建 10000 个 Machine,与 Processor 为多对多的关系。
M 从其绑定的 P 的本地队列中获取 G 并运行;
- work stealing:若其绑定的P中没有等待执行的 G,则从其他 M 绑定的P的本地队列中“偷”一半的 G;
- 当 M 从其他 P 中“偷”不到 G 时,尝试从全局队列中获取 G;
- 当 M 获取不到可运行的 G 时,当前 M 会与 P 分离并被放入 M 的 Waiting 队列中等待被其他 P 唤醒;
hand off:当 M 发生阻塞并阻塞超时,M 会尝试与 P 分离,并将 P 转移给其它空闲的 M 执行;
Processor
用来承载多个 Goroutine 的运行器,与 Machine 为多对多的关系。
P 唤醒一个 M,如果 M 的 Waiting 队列中没有等待被唤醒的 M,则在不超过 M 最大数量的前提下创建一个新的 M 并与之绑定;
- 当 P 的本地队列为空且所属 M 无法从其他 P 中“偷”到 G 时,P 所属的 M 会与其分离,当前 P 会被放入 P 的 Waiting 队列中,等待被其他 G 唤醒;