GoLang协程Goroutiney原理与GMP模型详解
简介:
【11月更文挑战第4天】Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁开销小,适合高并发场景。其调度采用非抢占式和协作式多任务处理结合的方式。GMP 模型包括 G(Goroutine)、M(系统线程)和 P(逻辑处理器),通过工作窃取算法实现负载均衡,确保高效利用系统资源。
- Goroutine 原理
- Goroutine 是 Go 语言中并发执行的基本单位,它类似于线程,但比传统的线程更加轻量。在操作系统层面,线程的创建、销毁和切换都有一定的开销。而 Goroutine 是由 Go 运行时(runtime)管理的用户态线程,它的创建和销毁的开销非常小,使得在 Go 程序中可以轻松地创建成千上万个 Goroutine 来实现高并发。
- 例如,在一个处理网络请求的服务端程序中,可以为每个到来的请求创建一个 Goroutine 来处理,这样可以高效地处理大量并发请求。
- Goroutine 的调度是由 Go 运行时的调度器完成的。当一个 Goroutine 被创建后,它会被放入一个调度队列中等待执行。调度器会根据一定的策略从队列中选取 Goroutine 并将其分配到系统线程(OS Thread)上执行。
- 与操作系统的线程调度不同,Go 调度器采用了非抢占式多任务处理和协作式多任务处理相结合的方式。在非抢占式调度中,Goroutine 会一直执行,直到它主动让出 CPU 资源,比如通过调用
runtime.Gosched()
函数。而在某些特殊情况下,如 Goroutine 执行时间过长或者系统调用阻塞时,调度器也会进行抢占式调度,将 CPU 资源分配给其他等待的 Goroutine。
- GMP 模型详解
- G 代表 Goroutine,是 Go 程序中实际执行的代码片段。每个 Goroutine 都有自己的栈空间,初始栈大小一般比较小(通常为 2KB),并且可以根据需要动态增长。这与传统线程的固定栈大小相比,更有效地利用了内存资源。
- 例如,一个简单的 Goroutine 可能是一个函数的执行体,像
func() { println("Hello, Goroutine") }
这样的函数可以作为一个 Goroutine 来运行。
- M 代表系统线程,是操作系统层面的线程。Go 程序会利用操作系统的线程来执行 Goroutine。M 与内核线程直接相关,它负责执行 Goroutine 中的代码。Go 运行时会根据系统资源和 Goroutine 的数量来动态地创建和销毁 M。
- 例如,在一个多核 CPU 的系统中,Go 程序可能会创建多个 M 来充分利用多核的优势,每个 M 可以执行一个或多个 Goroutine。
- P 是一个抽象的概念,代表逻辑处理器。它是连接 G 和 M 的桥梁,每个 P 都有一个本地队列(Local Queue),用于存放等待执行的 Goroutine。P 的数量在程序启动时可以通过
GOMAXPROCS
环境变量或者runtime.GOMAXPROCS()
函数来设置,它通常与系统的 CPU 核心数相关,决定了 Go 程序可以同时并发执行的 Goroutine 的数量上限。
- 例如,假设
GOMAXPROCS
设置为 4,那么 Go 程序最多可以有 4 个 P,也就意味着最多可以同时有 4 个 Goroutine 在不同的 CPU 核心上执行(不考虑超线程等情况)。
- 当一个 Goroutine 被创建时,它会首先被放入一个 P 的本地队列中。M 会从 P 的本地队列中获取 Goroutine 来执行。如果 P 的本地队列中没有 Goroutine 了,M 会尝试从其他 P 的队列中 “窃取” Goroutine 来执行(工作窃取算法),这样可以保证各个 P 之间的负载均衡。
- 当 M 因为系统调用等原因阻塞时,它会释放绑定的 P,使得 P 可以与其他 M 绑定继续执行 Goroutine。而当 M 完成系统调用返回后,它会尝试获取一个空闲的 P 或者从其他 P 那里 “窃取” Goroutine 来继续执行。这种机制保证了 Go 程序在面对各种复杂的系统调用和阻塞情况时,仍然能够高效地利用系统资源,保持 Goroutine 的高效调度。