目录
前言
Goroutine是Golang中实现的协程,它保证了Golang的高并发的可用性,这些Goroutine分配、负载、调度到处理器上采用的是G-M-P模型。
进程、线程、Goroutine
进程是操作系统进行资源分配调度的最小单元,而线程则是CPU进行调度的最小单元。进程和线程的切换管理需要操作系统接管,因此在切换时会涉及到CPU的上下文切换,会导致切换的成本较大。而Goroutine作为用户级线程在切换时有用户控制,因此切换成本没有内核级线程大。并且,内核级线程是抢占式调度,但是用户级线程是协作式调度(一个协程让出CPU使用权之后另一个协程才能被执行)。
内核级线程
下图表示为内核级线程示意图:
内核级线程模型中用户线程与内核线程是一对一关系(1:1)。线程的创建、销毁、切换工作通过内核完成的。 应用程序不参与线程的管理工作,只能通过调用内核级线程编程接口完成用户线程的管理。每个用户线程都会被绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,内核线程将随之一起离开系统。
内核级线程模型有如下优点:
- 在多处理器系统中,内核能够并行执行同一进程内的多个线程
-
如果进程中的一个线程被阻塞,不会阻塞其他线程,是能够切换同一进程内的其他线程继续执行
-
在内核级线程中,当一个线程阻塞时,内核根据选择可以运行另一个进程的线程;在用户级线程中,运行时系统始终运行自己进程中的线程
缺点:
- 线程的创建与删除都需要CPU参与,成本大
用户级线程
下图表示为用户级线程示意图:
用户线程模型中的用户线程与内核线程KSE是多对一关系(N:1)。线程的创建、销毁以及线程之间的协调、同步等工作都是在用户态完成,具体来说就是由应用程序的线程库来完成。 内核对这些是无感知的,内核此时的调度都是基于进程的。线程的并发处理从宏观来看,任意时刻每个进程只能够有一个线程在运行,且只有一个处理器内核会被分配给该进程。
从上图中可以看出来:库调度器从进程的多个线程中选择一个线程,然后该线程和该进程允许的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程是一种”多对一”的线程映射
用户级线程有如下优点:
- 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多, 因为保存线程状态的过程和调用程序都只是本地过程
-
线程能够利用的表空间和堆栈空间比内核级线程多
缺点:
- 如果线程发生I/O或页面故障引起的阻塞时,此时在系统内核会发生阻塞,对于内核而言,并不知道有多线程的存在,婴儿会阻塞整个进程从而阻塞所有线程, 因此同一进程中只能同时有一个线程在运行
-
同一个进程下的多个线程只能分时复用
两级线程模型
下图表示为两级线程模型
两级线程模型中用户线程与内核线程是一对一关系(N : M)。 两级线程模型充分吸收上面两种模型的优点,尽量规避缺点。其线程创建在用户空间中完成,线程的调度和同步也在应用程序中进行。一个应用程序中的多个用户级线程被绑定到一些(小于或等于用户级线程的数目)内核级线程上。避免了内核级线程一对一的关系,同时也避免了用户级线程的当中N对一的关系。
Golang的线程模型
Golang当中的线程模型为两级混合模型,其中Processor为逻辑处理器,Machine为系统线程,由系统调用产生,每一个M都对应一个内核调度实体(Kernal Scheduling Entity),Goroutine为用户线程
下图为GMP调度示意图:
- 其中G为Goroutine,是被调度的最小单元;
-
P为Processor,是逻辑处理器。调度器的核心处理器,通常表示执行上下文,用于匹配 M 和 G 。P 的数量不能超过 GOMAXPROCS 配置数量,这个参数的默认值为CPU核心数;通常一个 P 可以与多个 M 对应,但同一时刻,这个 P 只能和其中一个 M 发生绑定关系;M 被创建之后需要自行在 P 的 free list 中找到 P 进行绑定,没有绑定 P 的 M,会进入阻塞态。每一个P最多关联256个G。
-
每个M(Machine)为系统级线程。
GMP的调度流程为:
-
M需要执行G,就需要和P关联,不关联无法获得G(P可以创建一个或多个M与之关联)
-
从P的本地队列中获取G
-
如果当前P没有可运行的G,那么M会尝试从全局队列获取一批G放入当前P的LRQ当中
-
如果全局队列没有G,那么M会随机从其他的P当中拿一半的G放到当前的P中本地队列。(PS: 全局运行队列承载本地队列“溢出”的G。为了保证调度公平性,调度过程中有 1/61 的几率优先检查全局队列,否则本地队列一直满载的情况下,全局队列中的G将永远无法被调度到;)
-
M获取G之后运行,之后获取下一个G。