1 协程的概念
1.1 基本概念
- 进程是应用程序的启动实例,每个进程都有独立的内存空间,不同进程通过进程间的通信方式来通信
- 线程从属于进程,每个进程至少包含一个线程,线程是CPU调度的基本单元,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。
- 协程可以理解为一种轻量级的线程,协程不受操作系统调度,协程调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行。
1.2 协程优势
在高并发应用中频繁创建线程会造成不必要的开销,所以由了线程池技术。使用的时候从中取出,使用完放回即可。
当线程在任务重发生系统调用时,会发生阻塞,则该线程会暂停执行任务。如果大量的线程都发生了系统调用,则会对系统发生阻塞,使任务得不到即使的处理。
- 增加线程池中线程的数量在一定程度上可以提高处理能力
- 但线程的增多,也会导致上下文切换的开销变大
那么当发生阻塞时,是否可以将当前任务放在一边,先执行其他任务呢?
- 工作在用户态的协程可以减少上下文切换的开销
- 通过协程调度器把可运行的协程逐个调度到线程中执行,同时及时把阻塞的协程调度出协程,从而有效避免了线程的频繁切换
- 达到使用少量线程实现高并发的效果
2 调度模型
2.1 线程模型
线程可分为用户线程和内核线程,用户线程由用户创建、同步和销毁,内核线程则由内核来管理。根据用户线程管理方式的不同,分为三种线程模型
- N:1模型
- 即N个用户线程运行在1个内核线程中,
- 优点是用户线程上下文切换快
- 缺点是无法充分利用CPU多核的算力
- 1:1模型
- 每个用户线程对应一个线程
- 优点是充分利用CPU的算力
- 缺点是线程上下文切换较慢
- M:N模型
- Go采用的是前两种模型的组合,M个用户线程(协程)运行在N个线程中
- 优点充分利用CPU的算力且协程上下文切换快
- 缺点是该模型的调度算法较为复杂
2.2 Go调度器模型
Go协程调度模型中包含三个关键实体,machine(M),processor(P)和goroutine(简称G)
- M(machine):工作线程,它由操作系统调度
- P(processor):处理器(Go定义的一个概念,不是指CPU的个数),包含运行go代码的必要资源,也有调度goroutine的能力
- G(goroutine):即go协程,每个go关键字都会创建一个协程
相关信息:
- M必须持有P才可以执行代码
- M也会被系统调用阻塞
- P的个数在程序启动时决定,默认情况下等同于CPU的核数。但可以通过GOMAXPROCS这个环境变量指定
一个简单的调度器模型如下图所示:
- 上图中包含2个工作线程M
- 每个M持有一个P
- 每个M中有一个协程在运行(已标记)
- 其他背景的协程正在等待被调度,每个处理器P拥有宇哥runqueues队列
- 此外还有一个全局的runqueues队列,由多个处理器共享
一般来说,处理器P中的协程G额外再创建的协程会被加入到本地的runqueues中;但如果本地的队列已满或阻塞的协程被唤醒,则会放入到全局的runqueues中;处理器除了调度本地的runqueues中的协程,还会周期性地从全局runqueues中摘取协程来调度。
3 调度策略
3.1 队列轮转
- 每个处理器P维护者一个协程G的队列,处理器P依次将协程G调度到M中执行
- 协程G执行结束后,处理器P会再次调度一个协程G到M中执行
- 同时P会周期性查看全局变量中是否有待运行的G,并将其调度到M中运行
- 全局变量中的G主要来自于系统调用中恢复的G
- 周期性查看全局队列的目的是为了防止全局队列中的G长时间得不到调度机会而被“饿死”
3.2 系统调用
- 协程发生系统调度时,对应的工作线程会被阻塞
- 前面提到P的个数默认等于CPU的个数,每个M必须持有一个P才可以执行G
- 一般情况下M个的个数会略大于P的个数。
- 多出来的M将会在G发生系统调用时发生作用。
- Go也提供了一个M的的池子,需要时从池子中获取,用完放回。不够时再创建。
- 当G0即将进入系统调用时
- M0释放P
- 冗余的M1获得P,继续执行P队列中剩下的G(冗余的M的来源可能是缓存池,也可能是新建的)
- M0由于陷入系统调用而被阻塞,M1接替了M0的工作
- 只要P不空闲,就可以充分利用CPU
- 当G0结束系统调用后,根据能否获得P,对G0进行不同的处理
- 如果有空闲的P,则获取一个P,继续执行G0
- 如果没有空闲的P,则将G0放入全局队列,等待被其他P调度,然后M0将进入缓冲休眠
3.3 工作量窃取
说白了就是当前P已经将自己队列中的任务全部干完了,而全局队列中又没有新的G。于是从别人的队列中获取一半的工作量。
3.4 抢占式调度
所谓抢占式调度,是指避免某个协程长时间执行,而阻碍其他协程被调用的机制。
- 调度器会监控每个协程执行的时间
- 时间过长且有其他协程子在等待时
- 会把协程暂停,转而调度等待的协程
3.5 GOMAXPROCS对性能的影响
一般来讲,程序运行时就将GOMAXPROCS的大小设置为CPU的核数,可让Go程序充分利用CPU。在某些I/O密集型的应用中,不妨把GOMAXPROCS的值设置得大一些,或许会有更好的效果。