目录

Go Goroutine

Goroutine 是 Go 语言并发模型的核心。它是一种由 Go 运行时调度的轻量级线程,创建成本极低(初始栈仅 2KB),可以轻松创建数十万个并发执行。通过 go 关键字即可启动一个 goroutine,让函数在后台并发执行。

并发 vs 并行

理解并发和并行的区别是学习 goroutine 的前提:

  • 并发(Concurrency):同时处理多件事(关注结构设计),是任务的逻辑组合
  • 并行(Parallelism):同时执行多件事(关注运行时执行),需要多核 CPU 支持

Go 的设计理念是"并发编程更容易"。通过 goroutine 和 channel,Go 让开发者可以用简洁的方式构建并发程序,运行时会自动将并发任务映射到可用的 CPU 核心上并行执行。

go 关键字启动 goroutine

在函数调用前加上 go 关键字,即可在新的 goroutine 中执行该函数。

package main

import (
	"fmt"
	"time"
)

func sayHello(name string) {
	for i := 0; i < 3; i++ {
		fmt.Printf("Hello from %s (goroutine)\n", name)
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	// 启动 goroutine
	go sayHello("goroutine-1")
	go sayHello("goroutine-2")

	// 主 goroutine 也在执行
	for i := 0; i < 3; i++ {
		fmt.Println("Hello from main")
		time.Sleep(100 * time.Millisecond)
	}

	// 等待 goroutine 完成(实际项目中应使用 WaitGroup)
	time.Sleep(500 * time.Millisecond)
}

每个 goroutine 都是独立执行的。主 goroutine(main 函数)退出时,程序结束,其他 goroutine 也会被终止。

goroutine 的栈空间

goroutine 的栈初始只有 2KB(OS 线程通常是 1-8MB),并且可以动态增长和收缩。这使得 Go 可以轻松创建大量 goroutine。

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func printGoroutineInfo(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Goroutine %d 正在执行\n", id)
}

func main() {
	// 查看当前 goroutine 数量
	fmt.Printf("初始 goroutine 数: %d\n", runtime.NumGoroutine())

	var wg sync.WaitGroup

	// 创建 100 个 goroutine
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go printGoroutineInfo(i, &wg)
	}

	fmt.Printf("创建后 goroutine 数: %d\n", runtime.NumGoroutine())

	wg.Wait()
	fmt.Printf("完成后 goroutine 数: %d\n", runtime.NumGoroutine())
}

100 个 goroutine 仅占用约 200KB 的栈空间,而相同数量的 OS 线程则需要数百 MB。

runtime.GOMAXPROCS

GOMAXPROCS 控制可以同时执行用户代码的 OS 线程数量,默认等于 CPU 核心数。

package main

import (
	"fmt"
	"runtime"
	"time"
)

func worker(id int, duration time.Duration) {
	start := time.Now()
	for time.Since(start) < duration {
		// 模拟计算密集型工作
	}
	fmt.Printf("Worker %d 完成\n", id)
}

func main() {
	fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
	fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 0 表示查询当前值

	// 设置 GOMAXPROCS(通常不需要手动设置)
	runtime.GOMAXPROCS(4)
	fmt.Printf("设置后 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))

	// 启动 8 个 worker
	for i := 0; i < 8; i++ {
		go worker(i, 100*time.Millisecond)
	}

	time.Sleep(200 * time.Millisecond)
}

在大多数情况下,保持默认的 GOMAXPROCS 即可。Go 运行时会自动管理线程调度。

GMP 调度模型

Go 的 goroutine 调度使用 GMP 模型:

https://img.zhaojq.top/20260729161728667.png
GMP调度模型

  • G(Goroutine):用户态的轻量级线程,包含执行栈、状态等信息
  • M(Machine):操作系统线程,由 OS 调度,负责执行 G
  • P(Processor):逻辑处理器,维护一个本地 goroutine 队列。P 的数量等于 GOMAXPROCS

调度流程:

  1. 每个 P 维护一个本地 G 队列
  2. M 必须绑定 P 才能执行 G
  3. 当 M 执行 G 发生阻塞(如系统调用)时,M 和 P 分离,P 会绑定新的 M
  4. 工作窃取:空闲的 P 会从其他 P 的队列中"窃取" G 来执行,实现负载均衡
package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	// 查看调度器统计信息
	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)

	fmt.Printf("Goroutine 数量: %d\n", runtime.NumGoroutine())
	fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
	fmt.Printf("CGo 调用次数: %d\n", stats.NumCgoCall)

	// 通过 trace 包可以查看调度细节
	// go tool trace 可以可视化 GMP 调度过程
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d 在运行\n", id)
		}(i)
	}
	wg.Wait()
}

sync.WaitGroup 等待 goroutine 完成

sync.WaitGroup 是等待一组 goroutine 完成的标准方式。

package main

import (
	"fmt"
	"sync"
	"time"
)

func downloadFile(id int, wg *sync.WaitGroup) {
	defer wg.Done() // 函数结束时计数器减 1

	fmt.Printf("开始下载文件 %d\n", id)
	time.Sleep(time.Duration(id*100) * time.Millisecond) // 模拟下载
	fmt.Printf("文件 %d 下载完成\n", id)
}

func main() {
	var wg sync.WaitGroup

	// 启动多个下载任务
	for i := 1; i <= 5; i++ {
		wg.Add(1) // 计数器加 1
		go downloadFile(i, &wg)
	}

	// 等待所有 goroutine 完成
	wg.Wait()
	fmt.Println("所有文件下载完成!")
}

WaitGroup 有三个方法:

  • Add(delta int):计数器增加 delta
  • Done():等价于 Add(-1)
  • Wait():阻塞直到计数器归零

注意:Add 应在 goroutine 启动之前调用,避免竞态条件。

goroutine 泄漏问题

goroutine 泄漏是指 goroutine 永远无法退出,持续占用资源。常见原因包括:

package main

import (
	"fmt"
	"runtime"
	"time"
)

// 泄漏示例 1:channel 阻塞(发送后无人接收)
func leakByChannel() {
	ch := make(chan int)
	go func() {
		ch <- 1 // 永远阻塞,无人接收
	}()
}

// 泄漏示例 2:无限循环无退出条件
func leakByLoop() {
	go func() {
		for {
			// 没有退出条件,永远运行
			time.Sleep(time.Second)
		}
	}()
}

// 正确做法:使用 context 或 done channel 控制退出
func safeGoroutine(done <-chan struct{}) {
	go func() {
		for {
			select {
			case <-done:
				fmt.Println("goroutine 安全退出")
				return
			default:
				// 执行工作
				time.Sleep(100 * time.Millisecond)
			}
		}
	}()
}

func main() {
	leakByChannel()
	leakByLoop()

	done := make(chan struct{})
	safeGoroutine(done)

	time.Sleep(300 * time.Millisecond)
	fmt.Printf("当前 goroutine 数: %d\n", runtime.NumGoroutine())

	close(done) // 通知 goroutine 退出
	time.Sleep(200 * time.Millisecond)
	fmt.Printf("退出后 goroutine 数: %d\n", runtime.NumGoroutine())
}

防止 goroutine 泄漏的最佳实践:

  1. 为每个 goroutine 设计明确的退出机制(done channel 或 context)
  2. 使用 context.WithTimeout 设置超时
  3. 使用 runtime.NumGoroutine() 监控 goroutine 数量
  4. 避免在 goroutine 中向无接收者的 channel 发送数据

总结

Goroutine 是 Go 并发的基石。它由 Go 运行时调度,初始栈仅 2KB,可以轻松创建数十万个。通过 go 关键字启动,使用 sync.WaitGroup 等待完成。Go 的 GMP 调度模型将 goroutine 高效映射到 OS 线程上执行,支持工作窃取实现负载均衡。使用 goroutine 时需注意避免泄漏,始终为 goroutine 提供退出机制。goroutine 配合 channel 构成了 Go 强大的并发编程能力。