跳转到内容

Go 协程goroutine

来自代码酷


Go协程(Goroutine)是Go语言中实现并发编程的核心机制,它是一种轻量级的线程,由Go运行时(runtime)管理,能够在极小的栈空间(初始仅2KB)上运行,并支持动态扩容。与操作系统线程相比,协程的创建和切换成本极低,使得开发者可以轻松创建成千上万的并发任务。

基本概念[编辑 | 编辑源代码]

Goroutine是Go语言对协程(Coroutine)的实现,但相比传统协程,它进一步与Go的调度器深度集成,支持多路复用操作系统线程(通过M:N调度模型)。其核心特点包括:

  • 轻量级:内存占用远小于线程(默认线程栈为1-8MB)。
  • 低成本创建与切换:由Go运行时调度,不依赖操作系统内核。
  • 通信通过Channel:遵循“不要通过共享内存来通信,而应通过通信来共享内存”的设计哲学。

与线程的对比[编辑 | 编辑源代码]

Goroutine vs 系统线程
特性 Goroutine 系统线程
创建成本 微秒级,栈可动态增长 毫秒级,固定栈大小
内存占用 初始2KB 通常1-8MB
调度方式 Go运行时抢占式调度 操作系统内核调度
通信机制 Channel(类型安全) 共享内存(需锁)

基本用法[编辑 | 编辑源代码]

通过go关键字即可启动一个Goroutine,语法如下:

go 函数名(参数列表)

示例:并发执行[编辑 | 编辑源代码]

以下代码展示主线程与Goroutine的并发执行:

package main

import (
	"fmt"
	"time"
)

func printNumbers() {
	for i := 1; i <= 3; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Printf("%d ", i)
	}
}

func main() {
	go printNumbers() // 启动Goroutine
	fmt.Println("Main starts")
	time.Sleep(500 * time.Millisecond) // 等待Goroutine完成
	fmt.Println("\nMain ends")
}

输出:

Main starts
1 2 3 
Main ends

关键点:

  • 主线程退出时,所有Goroutine会被强制终止(此处通过time.Sleep等待)。
  • Goroutine的执行顺序非确定性的(若移除Sleep,可能看不到输出)。

调度模型[编辑 | 编辑源代码]

Go使用M:N调度模型,其中:

  • M:操作系统线程(由内核管理)
  • N:Goroutine(用户态轻量线程)
  • P:逻辑处理器(绑定线程的上下文)

graph LR G1[Goroutine1] --> P[Processor] G2[Goroutine2] --> P P --> M[OS Thread] M --> CPU

调度器特点:

  • 工作窃取(Work Stealing):空闲P会从其他P的队列中偷取Goroutine。
  • 抢占式调度:防止单个Goroutine长时间占用CPU(Go 1.14+支持基于信号的抢占)。

同步与通信[编辑 | 编辑源代码]

Goroutine间通常通过channel通信,但也可使用sync包提供的锁机制。

使用Channel同步[编辑 | 编辑源代码]

func worker(done chan bool) {
	fmt.Print("Working...")
	time.Sleep(1 * time.Second)
	done <- true // 发送完成信号
}

func main() {
	done := make(chan bool)
	go worker(done)
	<-done // 阻塞直到收到信号
	fmt.Println("Done")
}

使用WaitGroup[编辑 | 编辑源代码]

适用于等待一组Goroutine完成:

var wg sync.WaitGroup

func task(id int) {
	defer wg.Done() // 等价于 wg.Add(-1)
	fmt.Printf("Task %d done\n", id)
}

func main() {
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go task(i)
	}
	wg.Wait() // 阻塞直到计数器归零
	fmt.Println("All tasks completed")
}

实际应用场景[编辑 | 编辑源代码]

1. Web服务器并发处理请求[编辑 | 编辑源代码]

每个HTTP请求在一个独立的Goroutine中处理:

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Request from %s", r.RemoteAddr)
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil) // 每个连接启动一个Goroutine
}

2. 并行计算[编辑 | 编辑源代码]

利用多核CPU加速计算(示例:并行计算斐波那契数列):

func fib(n int) int {
	if n <= 1 { return n }
	return fib(n-1) + fib(n-2)
}

func main() {
	const N = 40
	results := make(chan int, 2)

	go func() { results <- fib(N) }()
	go func() { results <- fib(N-1) }()

	total := <-results + <-results
	fmt.Println("Sum:", total)
}

高级主题[编辑 | 编辑源代码]

Goroutine泄漏[编辑 | 编辑源代码]

未正确退出的Goroutine会导致内存泄漏。常见原因:

  • 无缓冲Channel阻塞
  • 无限循环无退出条件

检测工具:

  • runtime.NumGoroutine()监控协程数量
  • 使用pprof进行性能分析

上下文(Context)控制[编辑 | 编辑源代码]

用于传递取消信号和超时控制:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

go func(ctx context.Context) {
	select {
	case <-time.After(3 * time.Second):
		fmt.Println("Work done")
	case <-ctx.Done():
		fmt.Println("Cancelled:", ctx.Err())
	}
}(ctx)

性能考量[编辑 | 编辑源代码]

  • GOMAXPROCS:控制使用的CPU核心数(默认等于逻辑CPU数)。
  • 避免过度创建Goroutine(虽然轻量,但并非无成本)。
  • 对于CPU密集型任务,需注意Amdahl定律的并行限制:

S=1(1P)+PN 其中P为并行比例,N为处理器数量。

总结[编辑 | 编辑源代码]

Goroutine是Go并发模型的核心,其轻量级特性和与Channel的深度整合,使得编写高并发程序变得简单高效。开发者需注意:

  • 优先使用Channel而非共享内存
  • 通过synccontext实现同步
  • 监控并防止Goroutine泄漏

掌握Goroutine是成为高效Go开发者的关键一步!