概述
Go 语言中的 goroutine 是一种轻量级线程, goroutine 之间通过 channel 进行通信。
使用 goroutine 可以方便地编写并发程序。
本文将介绍 goroutine 的使用方法、调度原理、同步处理及实战案例等内容。
主要内容包括
goroutine 简介
goroutine 使用方法
goroutine 调度原理
goroutine 同步处理
避免 goroutine 泄漏和死锁
goroutine 使用技巧
goroutine 实战案例
goroutine 源码分析
1、goroutine 简介
goroutine 是 Go 语言提供的轻量级线程,具有以下特点
每个 goroutine 占用内存很小,只有 2KB 左右
调度成本小,可以轻松创建上万个 goroutine
goroutine 通过 channel 通信传递数据
由 Go 运行时(runtime)自动调度执行
goroutine 让并发编程变得更简单高效。
2、goroutine 使用方法
使用go语句即可启动一个 goroutine,语法如下
go 函数名(参数列表)
例如
// 开启处理data的goroutinego process(data)
启动时可传递参数,参数需为原值或引用类型,否则参数副本无法修改
func sum(s []int, ch chan int) { sum := 0 for _, v := range s { sum += v } ch <- sum // 返回结果} go sum(s, resultCh)
匿名函数可直接启动 goroutine
go func() { // 使用外部变量 ...}()
3、goroutine 调度原理
Go 运行时通过 GMP 模型调度 goroutine
G - goroutine
P - processor,对应 OS 线程
M - machine,关联线程栈等执行上下文
GMP 共同协作完成 goroutine 调度执行,runtime 会根据负载动态分配 P 与 G。
runtime 采用以下技术优化调度:
复用线程,减少创建开销
工作窃取算法动态平衡负载
GOMAXPROCS 参数限制 CPU 使用数
4、goroutine 同步处理
1. WaitGroup
WaitGroup 可以等待一组 goroutine 完成
var wg sync.WaitGroup func process() { wg.Add(1) defer wg.Done() // do work } wg.Add(3) // 设置期望goroutine数go process() // 启动多个goroutinewg.Wait() // 等待结束
2. Channel
Channel 可用于 goroutine 间同步
ch := make(chan int) go func() { // 写入channel ch <- 1 }() // 从channel读取数据<-ch
可设置标志 channel 协调 goroutine
done := make(chan bool) go func() { done <- true}() <-done
5、避免 goroutine 泄漏和死锁
1. 泄漏
goroutine 可能发生泄漏
func main() { ch := make(chan int) go func() { <-ch // 可能一直阻塞 }()}
解决方法是在 main goroutine 退出前,关闭 channel
close(ch) // 主动关闭channel
2. 死锁
死锁场景
ch := make(chan int) func main() { go func() { ch <- 1 // 阻塞,等待接收 }() <-ch // 阻塞,等待发送 }
必须破坏死锁条件才能修复,如修改为单向 channel。
6、goroutine 使用技巧
优化 goroutine 使用的技巧
利用缓冲 channel 并发限速
避免过多阻塞与交互
合理设置 GOMAXPROCS
profiling 找出性能瓶颈
使用定时器防止泄漏
请注意不要启动过多阻塞的 goroutine 。
7、goroutine 实战案例
1.
func search(query string) { var wg sync.WaitGroup for _, site := range sites { wg.Add(1) go func(site string) { defer wg.Done() search(site, query) }(site) } wg.Wait() // 等待结束}
2.
func sort(items []int) { var wg sync.WaitGroup ch := make(chan []int) // 分割后子序列 channel // 分割为多个子序列 for i := range items { wg.Add(1) go func(sub []int) { defer wg.Done() sort(sub) // 排序子序列 ch <- sub // 返回排序结果 }(items[i:j]) } // 合并排序结果 for range items { sorted := <-ch // 添加到最终结果 } wg.Wait()}
8、goroutine 源码分析
goroutine 的调度实现位于 runtime 下的 proc.go 文件中,主要涉及下面几个结构
g 对象,代表 goroutine ,定义在 proc.go
type g struct { // goroutine状态 status int // goroutine运行栈信息 stack stack // 入口函数等信息 entry stack.entry}
m 对象,代表操作系统线程,在 proc.go
type m struct { // 绑定的操作系统线程id tid int // 调度相关信息 sched sched // caches缓存 caches cache // 关联的p p p}
p 对象,代表逻辑处理器,在 proc.go
type p struct { id int status uint32 // 本地可运行goroutine队列 runq gQueue }
runtime 会使用 g 结构体对象表示 goroutine, g 对象保存 goroutine 状态、调度信息等;
m 代表机器结构,维护与操作系统线程相关资源;
p 代表处理器逻辑单元,绑定特定 m,可运行 g;
runtime 会维护全局 g 队列及 p 的本地可运行 g 队列;
当创建 goroutine 时,会保存 goroutine 入口等信息到 g 对象;
findRunnable 函数会从队列中弹出待执行 g,放入 p 的本地队列;
retake 会尝试从其他 p 的队列窃取 g 来执行。
runtime 通过这些手段管理 goroutine 的调度和执行,实现高效的并发。
总结
goroutine 是 Go 语言轻量级线程,可以大规模并发。了解 goroutine 机制,正确使用是 Go 并发编程的关键。
本文从多个维度详细介绍了 goroutine 的工作原理、使用技巧等内容,可以帮助读者深入地理解和运用 goroutine ,编写高效的并发程序。