Go 实现的是两级线程模型(M:N),准确的说是 GMP 模型,是对两级线程模型的改进实现,使它能够更加灵活地进行线程之间的调度。
背景
含义 | 缺点 | |
单进程时代 | 每个程序就是一个进程,直到一个程序运行完,才能进行下一个进程 | 1. 无法并发,只能串行 2. 进程阻塞所带来的 CPU 时间浪费 |
多进程/线程时代 | 一个线程阻塞, CPU 可以立刻切换到其他线程中去执行 | 1. 进程/线程占用内存高 2. 进程/线程上下文切换成本高 |
协程时代 | 协程(用户态线程)绑定线程(内核态线程),CPU 调度线程执行 | 1. 实现起来较复杂,协程和线程的绑定依赖调度器算法 |
线程 -> CPU 由操作系统调度,协程 -> 线程由 Go 调度器来调度,协程与线程的映射关系有三种线程模型。
三种线程模型
线程实现模型主要分为:内核级线程模型
、用户级线程模型
、两级线程模型
,他们的区别在于用户线程与内核线程之间的对应关系。
1. 内核级线程模型(1:1)
1 个用户线程对应 1 个内核线程,这种最容易实现,协程的调度都由 CPU 完成了。
优点:
实现起来最简单
能够利用多核
如果进程中的一个线程被阻塞,不会阻塞其他线程,是能够切换同一进程内的其他线程继续执行
缺点:
上下文切换成本高,创建、删除和切换都由 CPU 完成
2. 用 户级线程模型(N:1)
1 个进程中的所有线程对应 1 个内核线程。
优点:
上下文切换成本低,在用户态即可完成协程切换
缺点:
无法利用多核
一旦协程阻塞,造成线程阻塞,本线程的其它协程无法执行
3. 两 级线程模型(M:N)
M 个线程对应 N 个内核线程。
优点:
能够利用多核
上下文切换成本低
如果进程中的一个线程被阻塞,不会阻塞其他线程,是能够切换同一进程内的其他线程继续执行
缺点:
实现起来最复杂
GMP 指的是 Go 语言运行时的三个关键组件:Goroutine、M(Machine)和 P(Processor)。
Goroutine 已经在前面的问题中讲到了,是 Go 语言中轻量级线程的实现,它可以在单个进程中同时执行多个任务,实现了并发编程。
M(Machine)是 Go 语言运行时的机器模型,它是操作系统线程(OS thread)和 Goroutine 之间的中间件。在 Go 语言中,每个 Goroutine 都会被分配到一个 M 上执行,而每个 M 只能同时执行一个 Goroutine,这是 Go 语言实现并发的关键之一。当一个 Goroutine 阻塞或者需要等待 I/O 操作时,对应的 M 会被回收,等待其它 Goroutine 上的任务。
P(Processor)是 Go 语言运行时的处理器,它负责调度 Goroutine 在 M 上运行,同时也负责管理 Goroutine 的队列、调度等工作。在 Go 语言中,P 的数量是可以配置的,默认情况下为机器的核心数,但是可以通过环境变量 GOMAXPROCS 来进行修改。
GMP 模型在 Go 语言中实现了一种高效的并发编程机制,它可以轻松地创建数以千计的 Goroutine,实现并发编程,而不会导致系统资源的耗尽。同时,GMP 模型也提供了一个高度灵活的调度器,可以自动地调整 Goroutine 的数量和 P 的数量,以适应不同的负载。
在 Go 1.0 之前,Go 语言的运行时使用的是 GM 调度模型,与现在的 GMP 调度模型有所不同。在 GM 模型中,M(Machine)和 P(Processor)被合并为一个单一的调度器,称为 G(Goroutine)调度器。
在 GM 模型中,所有的 Goroutine 都被分配到一个全局的 Goroutine 队列中,每个 M 都会从队列中取出一个 Goroutine 来执行。当一个 Goroutine 阻塞或者需要等待 I/O 操作时,对应的 M 会回收它,并从全局队列中取出另外一个 Goroutine 继续执行。这样,一个 M 可以执行多个 Goroutine,而不像现在的 GMP 模型一样只能执行一个。
GM 模型相对于 GMP 模型的优势是它的调度器更加简单,同时在低负载的情况下可以更加高效地利用系统资源。然而,GM 模型也存在一些问题,最大的问题是在高负载的情况下,由于所有的 Goroutine 都被放在全局队列中,导致竞争变得非常激烈,从而降低了并发性能。另外,GM 模型也无法支持多核 CPU 的并行执行,因为它只有一个单一的调度器。
因此,从 Go 1.0 开始,Go 语言的运行时采用了 GMP 调度模型,通过引入 M 和 P 的概念,实现了更加高效的并发编程机制,同时支持多核 CPU 的并行执行。