进程、线程、协程
进程:进程是系统进行资源分配的基本单元,有独立的内存空间
线程:线程是cpu调度和分派的基本单位,线程依附于进程存在,每个线程共享父进程的资源
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程切换只需要保存任务的上下文,没有内核的开销
协程与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
线程上下文切换
由于中断处理,多任务处理,用户态切换等等原因会导致CPU从一个线程切换到另一个线程,切换过程中要保存一个线程的状态切换到另一个线程的状态。
而这个上下文切换代价极高
为了解决传统的多线程问题Golang引入了Goroutine
Goroutine非常轻量:
- 上下文切换代价小:Goroutine上下文切换只涉及到三个寄存器(PC/SP/DX)的值的修改;而线程的上下文切换则需要设计模式转换(从用户态切换到内核态)、以及16个寄存器、PC、SP等寄存器的刷新
- 内存占用少:线程栈空间通常是2m,而goroutine栈空间最小2k(可最大扩展到1G)
Go并发调度的GMP模型
在操作系统提供的内核线程之上,Go搭建了一个特有的两级线程模型。goroutine机制实现了M : N的线程模型
,goroutine机制是协程(coroutine)的一种实现,golang内置的调度器,可以让多核CPU中每个CPU执行一个协程。
Go的调度器是如何工作的
Go语言中支撑整个scheduler实现的主要有4个重要结构,分别是M、G、P、Sched, 前三个定义在runtime.h中,Sched定义在proc.c中。
- Sched结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。
- M结构是Machine,系统线程,它由操作系统管理的,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。
- P结构是Processor,处理器,它的主要用途就是用来执行goroutine的,它维护了一个goroutine队列,即runqueue。Processor是让我们从N:1调度到M:N调度的重要部分。
- G是goroutine实现的核心结构,它包含了栈,指令指针,以及其他对调度goroutine很重要的信息,例如其阻塞的channel。
Processor的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者通过运行时调用函数GOMAXPROCS()进行设置。Processor数量固定意味着任意时刻只有GOMAXPROCS个线程在运行go代码。
我们分别用三角形,矩形和圆形表示Machine Processor和Goroutine。
GMP在单核处理器的场景下,所有goroutine运行在同一个M系统线程中,每一个M系统线程维护一个Processor,任何时刻,一个Processor中只有一个goroutine,其他goroutine在runqueue中等待。一个goroutine运行完自己的时间片后,让出上下文,回到runqueue中。 多核处理器的场景下,为了运行goroutines,每个M系统线程会持有一个Processor。
单核
Go运行时系统中的调度器的主要职责就是将G公平合理的安排到多个M上去执行
在正常情况下,scheduler会按照上面的流程进行调度,但是为了充分利用线程的计算资源,Go调度器采取以下几种策略
任务窃取
为了提高Go并行处理能力,提高整体效率,当每个p之间的G任务不均衡时,调度器允许从GRQ(全局运行队列)LRQ(本地运行队列)中获取G
任务窃取
减少阻塞
当正在运行的goroutine阻塞的时候,例如进行系统调用,会再创建一个系统线程(M1),当前的M线程放弃了它的Processor,P转到新的线程中去运行
阻塞
- 由于原子操作、互斥量、通道操作导致的阻塞,调度器将当前阻塞Goroutine切换出去,重新调度LRQ上的其他Goroutine
- 由于网络请求阻塞 Go提供了网络轮询器(NetPoller)来处理网络请求和I/O操作的问题,通过NetPoller来进行网络系统调用,调度器可以防止Goroutine在进行这些操作时阻塞M。可以让M执行P中的LRQ的其他Goroutine和不需要创建M1,有助于减少操作系统上的负载。
网友评论