Goroutine 是 Go 编程语言中一个极具特色的设计,也是其并发能力的核心亮点之一。Goroutine 本质上是一种协程(Coroutine),是实现并行计算的关键。使用 Goroutine 非常简单,只需通过 go 关键字即可启动一个协程,协程会以异步方式运行。程序无需等待 Goroutine 完成即可继续执行后续代码。
go func() // 使用 go 关键字启动一个协程
II. Goroutine 的内部原理
概念介绍
并发(Concurrency)
在单个 CPU 上,可以同时执行多个任务。在极短的时间内,CPU 会在任务之间快速切换(例如,先执行一小段程序 A,然后迅速切换到程序 B)。从宏观上看,这种任务的时间上有重叠,似乎是同时执行的,但从微观上看,实际上是顺序执行的。这种现象称为并发。
并行(Parallelism)
当系统拥有多个 CPU 时,每个 CPU 可以同时运行任务,且各自不需要争夺资源。多个任务真正同时运行,这种现象称为并行。
进程(Process)
当 CPU 在多个程序之间切换时,如果不保存之前程序的状态(即上下文),直接切换到下一个程序,那么之前程序的一系列状态会丢失。为了解决这个问题,引入了进程的概念。进程为程序执行分配所需的资源,因此进程是程序运行的基本资源单位(也可以看作程序执行的实体)。例如,运行一个文本编辑器时,该进程会管理所有资源,如文本缓冲区的内存空间、文件操作资源等。
线程(Thread)
CPU 在多个进程之间切换时,由于需要进入内核模式并读取用户模式数据,切换开销较大。随着进程数量增加,CPU 调度会消耗大量资源。为了解决这一问题,引入了线程的概念。线程本身消耗的资源很少,它们共享进程内的资源。线程的调度开销比进程小得多。例如,在一个 Web 服务器应用中,可以使用多个线程同时处理不同的客户端请求,这些线程共享服务器进程的资源(如网络连接和内存缓存)。
协程(Coroutine)
协程拥有自己的寄存器上下文和栈。当协程被调度切换时,会保存当前的寄存器上下文和栈;当切换回来时,则恢复之前保存的上下文和栈。因此,协程可以保留上一次调用的状态(即所有局部状态的特定组合)。每次重新进入协程时,相当于返回到上次调用时的状态,即逻辑流程中上次退出的位置。
线程和进程的操作由系统接口触发,最终由系统执行;而协程的操作由用户程序自身执行。Goroutine 就是一种协程。
调度模型简介
Goroutine 的强大并发能力通过 GPM 调度模型实现。以下是 Goroutine 调度模型的核心结构:
调度器中的四个重要结构
- M(Machine)表示内核级线程。每个 M 对应一个线程,Goroutine 运行在 M 上。例如,当一个 Goroutine 被启动以执行复杂计算时,该 Goroutine 会被分配到一个 M 上执行。M 是一个较大的结构,包含小对象内存缓存(mcache)、当前正在执行的 Goroutine、随机数生成器等信息。
- G(Goroutine)表示 Goroutine。它有自己的栈,用于存储函数调用信息,还有一个指令指针,用于指定执行位置。此外,G 还包含其他信息(如等待的通道信息),这些信息用于调度。例如,当一个 Goroutine 等待从通道接收数据时,该信息会存储在 G 结构中。
- P(Processor)全称为 Processor,主要用于执行 Goroutine。可以将其视为任务分发器。P 维护一个 Goroutine 队列,存储需要由其执行的所有 Goroutine。例如,当创建多个 Goroutine 时,这些 Goroutine 会被添加到 P 的队列中等待调度。
- Sched(Scheduler)表示调度器。可以看作是中央调度中心,维护 M 和 G 的队列,以及调度器的一些状态信息,确保整个系统的高效调度。
调度的实现
调度模型图
如图所示,有两个物理线程 M,每个 M 绑定一个处理器 P,并运行一个 Goroutine。
- P 的数量可以通过 GOMAXPROCS() 设置。它实际上表示真正的并发级别,即可以同时运行的 Goroutine 数量。
- 图中灰色的 Goroutine 尚未运行,处于就绪状态,等待被调度。P 维护了这些 Goroutine 的队列(称为运行队列 runqueue)。
- 在 Go 语言中,启动一个 Goroutine 非常简单:只需使用 go function。每次执行 go 语句时,都会将一个 Goroutine 添加到运行队列末尾。在下一个调度点,会从运行队列中取出一个 Goroutine 执行。
当某个操作系统线程(如 M0)被阻塞时(如下图所示),P 会切换到另一个线程(如 M1)。M1 可能是新创建的,也可能是从线程缓存中取出的。
线程阻塞切换图
当 M0 返回时,它需要尝试获取一个 P 来运行 Goroutine。如果无法获取 P,它会将 Goroutine 放入全局运行队列,并进入休眠状态(进入线程缓存)。所有 P 会定期检查全局运行队列,并运行其中的 Goroutine;否则,全局运行队列中的 Goroutine 将永远无法执行。
III. Goroutine 的使用
基本用法
设置 Goroutine 的运行 CPU 数量。Go 的最新版本默认会自动设置。
num := runtime.NumCPU() // 获取主机的逻辑 CPU 数量
runtime.GOMAXPROCS(num) // 根据主机 CPU 数量设置 Goroutine 的最大并发级别
使用示例
示例 1:简单的 Goroutine 计算
package main
import (
"fmt"
"time"
)
func cal(a int, b int) {
c := a + b
fmt.Printf("%d + %d = %d\n", a, b, c)
}
func main() {
for i := 0; i < 10; i++ {
go cal(i, i+1) // 启动 10 个 Goroutine 进行计算
}
time.Sleep(time.Second * 2) // 等待所有任务完成
}
运行结果:
8 + 9 = 17
9 + 10 = 19
4 + 5 = 9
...
Goroutine 异常捕获
当启动多个 Goroutine 时,如果其中一个发生异常且未处理,整个程序会终止。因此,建议在每个 Goroutine 的函数中添加异常处理。可以使用 recover 函数捕获异常。
package main
import (
"fmt"
"time"
)
func addele(a []int, i int) {
deferfunc() {
if err := recover(); err != nil {
fmt.Println("add ele fail")
}
}()
a[i] = i
fmt.Println(a)
}
func main() {
Arry := make([]int, 4)
for i := 0; i < 10; i++ {
go addele(Arry, i)
}
time.Sleep(time.Second * 2)
}
运行结果:
add ele fail
[0 0 0 0]
[0 1 0 0]
...
Goroutine 的同步
由于 Goroutine 是异步执行的,主程序可能在 Goroutine 完成前退出。为确保所有 Goroutine 完成后再退出,Go 提供了 sync 包和 channel 来解决同步问题。
示例 1:使用 sync.WaitGroup 同步 Goroutine
package main
import (
"fmt"
"sync"
)
func cal(a int, b int, n *sync.WaitGroup) {
c := a + b
fmt.Printf("%d + %d = %d\n", a, b, c)
defer n.Done()
}
func main() {
var go_sync sync.WaitGroup
for i := 0; i < 10; i++ {
go_sync.Add(1)
go cal(i, i+1, &go_sync)
}
go_sync.Wait()
}
运行结果:
9 + 10 = 19
2 + 3 = 5
...
Goroutine 间的通信
Goroutine 本质上是协程,可以通过 channel 实现通信或数据共享。
示例:使用 channel 模拟生产者-消费者模式
package main
import (
"fmt"
"sync"
)
func Productor(mychan chan int, data int, wait *sync.WaitGroup) {
mychan <- data
fmt.Println("product data:", data)
wait.Done()
}
func Consumer(mychan chan int, wait *sync.WaitGroup) {
a := <-mychan
fmt.Println("consumer data:", a)
wait.Done()
}
func main() {
datachan := make(chanint, 100)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go Productor(datachan, i, &wg)
}
for j := 0; j < 10; j++ {
wg.Add(1)
go Consumer(datachan, &wg)
}
wg.Wait()
}
运行结果:
product data: 0
consumer data: 0
...