当前位置: 首页 > news >正文

4.3 Go 协程:goroutine

目录

  • 一、Goroutine 是什么?
    • 1. Goroutine vs 操作系统线程
    • 2. 并发 (Concurrency) 与并行 (Parallelism)
  • 二、如何启动 Goroutine
    • 1. go 关键字的使用
    • 2. 示例:主协程与子协程
    • 3. 匿名函数与参数传递
  • 三、Goroutine 的生命周期
    • 1. Goroutine 的状态
    • 2. Goroutine 的 Panic 处理
  • 四、Go 调度器原理
    • 1. GMP 模型
    • 2. 调度器的工作机制
  • 五、常见陷阱与最佳实践
    • 1. 循环变量陷阱
    • 2. Goroutine 泄漏
    • 3. 性能优化建议
  • 七、Goroutine 的应用
  • 八、总结

Go 语言将 并发作为其核心特性,通过 Goroutine(协程) 机制,让开发者能够轻松编写出高性能且易于理解的并发程序。

一、Goroutine 是什么?

Goroutine 是 Go 语言并发的执行单元。你可以简单地将其视为一个轻量级的线程

在 Go 语言中,每一个并发的执行单元都称为 Goroutine (协程)。

想象一下您的程序中有两个独立的功能函数,一个负责复杂的计算任务,另一个负责将结果输出。在一个传统的、线性的程序中,这两个函数必须依次执行——先完成计算,再进行输出。

而当您使用 Goroutine 时,这两个函数可以同时开始、独立运行。Go 语言的运行时(Runtime)会自动调度它们,让您能够轻松地实现并发执行。

你可以将 Goroutine 简单地理解为一个“超级轻量级”的线程。 相比于操作系统线程的高开销,Goroutine 的创建和管理成本极低,这使得 Go 程序能够轻松启动成千上万个并发任务。

1. Goroutine vs 操作系统线程

在这里插入图片描述
总结: Goroutine 相比传统线程更加轻量、高效,使得 Go 语言能以极低的成本实现大规模并发。

2. 并发 (Concurrency) 与并行 (Parallelism)

  • 并发 (Concurrency): 指的是处理多个任务的能力。宏观上看起来是同时在做,但微观上可能在单核 CPU 上通过时间片切换轮流执行。Go 语言的设计目标是实现并发。
  • 并行 (Parallelism): 指的是同时做多个任务的能力。这需要多个 CPU 核心同时运行多个任务。
  • Go Runtime: Go 语言的运行时会自动将 Goroutine 高效地映射到少量的操作系统线程上,通过调度器实现并发,如果有多核 CPU 也会自动实现并行。

二、如何启动 Goroutine

启动 Goroutine 非常简单,只需在一个函数调用前加上 go 关键字。

1. go 关键字的使用

任何函数或匿名函数,只要在调用前加上 go 关键字,就会在一个新的 Goroutine 中执行。

// 1. 普通函数调用
func myFunction() {// ...
}// 启动一个 Goroutine 执行 myFunction
go myFunction()

2. 示例:主协程与子协程

当一个 Go 程序启动时,main 函数运行在一个单独的 Goroutine 中,我们称之为 main Goroutine。

注意: 当 main Goroutine 结束时,程序会立即终止,不管其他 Goroutine 是否仍在运行。

package mainimport ("fmt""time"
)func worker(id int) {fmt.Printf("Goroutine %d: 开始工作...\n", id)time.Sleep(500 * time.Millisecond) // 模拟耗时操作fmt.Printf("Goroutine %d: 完成工作。\n", id)
}func main() {// 启动两个子协程go worker(1) go worker(2)fmt.Println("Main Goroutine: 已启动所有子协程。")// 阻塞 main Goroutine 一段时间,确保子协程有时间运行// ⚠️ 警告:这是一种临时的、不推荐的同步方式time.Sleep(1 * time.Second) fmt.Println("Main Goroutine: 退出。") // main 函数返回,程序结束,即使 worker 协程尚未完全完成
}

输出示例:

Main Goroutine: 已启动所有子协程。
Goroutine 2: 开始工作...
Goroutine 1: 开始工作...
Goroutine 1: 完成工作。
Goroutine 2: 完成工作。
Main Goroutine: 退出。

重要提示:

在实际项目中,绝对不能使用 time.Sleep 来等待 Goroutine 完成。这是一种不确定的同步方式。

更优雅且推荐的方式是使用 sync.WaitGroup 或 Channel 来进行同步

3. 匿名函数与参数传递

Goroutine 也可以通过匿名函数启动,通常用于需要传递参数或访问闭包变量的场景。

package mainimport ("fmt""time"
)func main() {for i := 0; i < 5; i++ {// ❌ 错误:i 的值在 Goroutine 执行时可能已经改变(全部输出 5)// go func() {//     fmt.Printf("循环变量i: %d\n", i)// }()// ✅ 正确做法:将 i 作为参数传递给匿名函数,保证 Goroutine 捕获到当前 i 的值go func(j int) {fmt.Printf("Goroutine %d: 正在执行...\n", j)}(i) // 立即调用匿名函数并传入 i 的值}time.Sleep(100 * time.Millisecond)fmt.Println("所有 Goroutine 启动完毕。")
}

三、Goroutine 的生命周期

一个 Goroutine 的生命周期从go语句开始,并在以下情况之一结束:

  • 函数自然返回: Goroutine 内部执行的函数正常执行完毕,返回。
  • 发生未捕获的 panic: 如果 Goroutine 发生 panic 且没有在自身的 defer 中被 recover 捕获,则该 Goroutine 终止,并导致整个程序崩溃。
  • 主 Goroutine 结束: 当 main 函数返回时,程序会强制终止所有正在运行的 Goroutine。

1. Goroutine 的状态

package mainimport ("fmt""runtime""time"
)func showGoroutineInfo() {fmt.Printf("当前 Goroutine 数量: %d\n", runtime.NumGoroutine())
}func longRunningTask(id int) {fmt.Printf("Goroutine %d: 开始执行\n", id)// 模拟长时间运行的任务for i := 0; i < 5; i++ {time.Sleep(200 * time.Millisecond)fmt.Printf("Goroutine %d: 执行步骤 %d\n", id, i+1)}fmt.Printf("Goroutine %d: 执行完成\n", id)
}func main() {fmt.Println("程序开始")showGoroutineInfo() // 显示当前只有 main goroutine// 启动多个 goroutinefor i := 1; i <= 3; i++ {go longRunningTask(i)}// 稍等片刻,让 goroutine 开始执行time.Sleep(100 * time.Millisecond)showGoroutineInfo() // 显示增加的 goroutine 数量// 等待所有 goroutine 完成(这里用 sleep 仅作演示)time.Sleep(2 * time.Second)showGoroutineInfo() // 显示 goroutine 完成后的数量fmt.Println("程序结束")
}

2. Goroutine 的 Panic 处理

package mainimport ("fmt""time"
)func safeworker(id int) {// 使用 defer 和 recover 捕获 panicdefer func() {if r := recover(); r != nil {fmt.Printf("Goroutine %d: 捕获到 panic: %v\n", id, r)}}()fmt.Printf("Goroutine %d: 开始工作\n", id)if id == 2 {panic("模拟错误") // 故意触发 panic}time.Sleep(500 * time.Millisecond)fmt.Printf("Goroutine %d: 正常完成\n", id)
}func unsafeWorker(id int) {fmt.Printf("Unsafe Goroutine %d: 开始工作\n", id)if id == 2 {panic("未处理的 panic") // 这会导致整个程序崩溃}time.Sleep(500 * time.Millisecond)fmt.Printf("Unsafe Goroutine %d: 正常完成\n", id)
}func main() {fmt.Println("=== 安全的 Goroutine 示例 ===")// 启动安全的 goroutine(有 panic 处理)for i := 1; i <= 3; i++ {go safeworker(i)}time.Sleep(1 * time.Second)fmt.Println("所有安全的 Goroutine 处理完成")// 注意:下面的代码会导致程序崩溃,仅作演示// fmt.Println("\n=== 不安全的 Goroutine 示例 ===")// for i := 1; i <= 3; i++ {//     go unsafeWorker(i)// }// time.Sleep(1 * time.Second)
}

四、Go 调度器原理

1. GMP 模型

Go 语言的调度器采用 GMP 模型:

  • G (Goroutine): 协程,用户态的轻量级线程
  • M (Machine): 操作系统线程,真正执行计算的资源
  • P (Processor): 处理器,调度的上下文,维护 Goroutine 队列
package mainimport ("fmt""runtime""time"
)func showRuntimeInfo() {fmt.Printf("GOMAXPROCS (P的数量): %d\n", runtime.GOMAXPROCS(0))fmt.Printf("NumCPU (CPU核心数): %d\n", runtime.NumCPU())fmt.Printf("NumGoroutine (当前Goroutine数): %d\n", runtime.NumGoroutine())
}func cpuBoundTask(id int) {fmt.Printf("CPU密集型任务 %d 开始\n", id)// 模拟 CPU 密集型计算count := 0for i := 0; i < 1000000000; i++ {count++}fmt.Printf("CPU密集型任务 %d 完成,计算结果: %d\n", id, count)
}func ioBoundTask(id int) {fmt.Printf("IO密集型任务 %d 开始\n", id)// 模拟 IO 等待time.Sleep(1 * time.Second)fmt.Printf("IO密集型任务 %d 完成\n", id)
}func main() {fmt.Println("=== 运行时信息 ===")showRuntimeInfo()fmt.Println("\n=== CPU密集型任务测试 ===")start := time.Now()// 启动多个 CPU 密集型任务for i := 1; i <= 4; i++ {go cpuBoundTask(i)}time.Sleep(3 * time.Second) // 等待完成fmt.Printf("CPU密集型任务耗时: %v\n", time.Since(start))fmt.Println("\n=== IO密集型任务测试 ===")start = time.Now()// 启动多个 IO 密集型任务for i := 1; i <= 10; i++ {go ioBoundTask(i)}time.Sleep(2 * time.Second) // 等待完成fmt.Printf("IO密集型任务耗时: %v\n", time.Since(start))
}

2. 调度器的工作机制

package mainimport ("fmt""runtime""sync/atomic""time"
)var counter int64func increment(id int) {for i := 0; i < 1000; i++ {atomic.AddInt64(&counter, 1)// 主动让出 CPU 时间片if i%100 == 0 {runtime.Gosched()}}fmt.Printf("Goroutine %d 完成\n", id)
}func main() {fmt.Printf("初始 GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))// 启动多个 goroutine 进行计数numGoroutines := 10for i := 1; i <= numGoroutines; i++ {go increment(i)}// 监控计数器变化for i := 0; i < 10; i++ {time.Sleep(100 * time.Millisecond)fmt.Printf("当前计数: %d, Goroutine数: %d\n", atomic.LoadInt64(&counter), runtime.NumGoroutine())}fmt.Printf("最终计数: %d\n", atomic.LoadInt64(&counter))
}

五、常见陷阱与最佳实践

1. 循环变量陷阱

package mainimport ("fmt""time"
)func demonstrateLoopTrap() {fmt.Println("=== 错误示例:循环变量陷阱 ===")// ❌ 错误:所有 goroutine 可能打印相同的值for i := 0; i < 5; i++ {go func() {fmt.Printf("错误示例 - i = %d\n", i) // 可能全部打印 5}()}time.Sleep(100 * time.Millisecond)fmt.Println("\n=== 正确示例:参数传递 ===")// ✅ 正确:通过参数传递当前值for i := 0; i < 5; i++ {go func(val int) {fmt.Printf("正确示例 - val = %d\n", val)}(i)}time.Sleep(100 * time.Millisecond)fmt.Println("\n=== 正确示例:局部变量 ===")// ✅ 正确:创建局部变量for i := 0; i < 5; i++ {i := i // 创建新的局部变量go func() {fmt.Printf("局部变量示例 - i = %d\n", i)}()}time.Sleep(100 * time.Millisecond)
}func main() {demonstrateLoopTrap()
}

2. Goroutine 泄漏

package mainimport ("context""fmt""runtime""time"
)// ❌ 错误:可能导致 goroutine 泄漏
func leakyGoroutine() {go func() {for {// 无限循环,没有退出条件time.Sleep(1 * time.Second)fmt.Println("泄漏的 goroutine 仍在运行...")}}()
}// ✅ 正确:使用 context 控制 goroutine 生命周期
func controlledGoroutine(ctx context.Context) {go func() {ticker := time.NewTicker(1 * time.Second)defer ticker.Stop()for {select {case <-ctx.Done():fmt.Println("受控的 goroutine 正常退出")returncase <-ticker.C:fmt.Println("受控的 goroutine 正在工作...")}}}()
}func main() {fmt.Printf("初始 Goroutine 数量: %d\n", runtime.NumGoroutine())// 演示 goroutine 泄漏(注释掉以避免实际泄漏)// leakyGoroutine()// 使用 context 控制 goroutinectx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()controlledGoroutine(ctx)time.Sleep(4 * time.Second)fmt.Printf("最终 Goroutine 数量: %d\n", runtime.NumGoroutine())
}

3. 性能优化建议

package mainimport ("fmt""runtime""sync""time"
)// 测试不同数量的 goroutine 性能
func benchmarkGoroutines(numGoroutines int) time.Duration {var wg sync.WaitGroupstart := time.Now()for i := 0; i < numGoroutines; i++ {wg.Add(1)go func(id int) {defer wg.Done()// 模拟轻量工作time.Sleep(1 * time.Millisecond)}(i)}wg.Wait()return time.Since(start)
}func main() {fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0))// 测试不同数量的 goroutinetestCases := []int{10, 100, 1000, 10000}for _, num := range testCases {duration := benchmarkGoroutines(num)fmt.Printf("%d 个 Goroutine 耗时: %v\n", num, duration)}// 性能建议fmt.Println("\n=== 性能优化建议 ===")fmt.Println("1. 避免创建过多的 goroutine,考虑使用工作池模式")fmt.Println("2. CPU 密集型任务的 goroutine 数量不应超过 CPU 核心数")fmt.Println("3. IO 密集型任务可以创建更多的 goroutine")fmt.Println("4. 使用 sync.Pool 重用对象,减少 GC 压力")fmt.Println("5. 合理设置 GOMAXPROCS,通常等于 CPU 核心数")
}

七、Goroutine 的应用

以下是一个使用协程解决计算密集型任务效率问题的例子:计算多个整数的素数分解

不使用协程的下载示例:

package mainimport ("fmt""time"
)// factorize 对一个整数进行素数分解
func factorize(num int) []int {factors := []int{}for i := 2; i*i <= num; i++ {time.Sleep(100 * time.Microsecond) // 模拟耗时任务for num%i == 0 {factors = append(factors, i)num /= i}}if num > 1 {factors = append(factors, num)}return factors
}func main() {numbers := make([]int, 10000)for i := range numbers {numbers[i] = i + 1}start := time.Now()for _, num := range numbers {factors := factorize(num)fmt.Printf("Factors of %d: %v\n", num, factors)}elapsed := time.Since(start)fmt.Printf("Total time without goroutines: %v\n", elapsed)
}

使用协程的素数分解:

package mainimport ("fmt""sync""time"
)// factorize 对一个整数进行素数分解
func factorize(num int) []int {factors := []int{}for i := 2; i*i <= num; i++ {time.Sleep(100 * time.Microsecond) // 模拟耗时任务for num%i == 0 {factors = append(factors, i)num /= i}}if num > 1 {factors = append(factors, num)}return factors
}func main() {numbers := make([]int, 10000)for i := range numbers {numbers[i] = i + 1}var wg sync.WaitGroupstart := time.Now()// 创建一个容量为 len(numbers) 的 channel 用于接收素数分解结果resultCh := make(chan []int, len(numbers))for _, num := range numbers {wg.Add(1)go func(n int) {defer wg.Done()factors := factorize(n)resultCh <- factors}(num)}go func() {wg.Wait()close(resultCh)}()for factors := range resultCh {fmt.Printf("Factors of %d: %v\n", numbers[len(resultCh)], factors)}elapsed := time.Since(start)fmt.Printf("Total time with goroutines: %v\n", elapsed)
}

八、总结

Goroutine 是 Go 语言并发编程的核心,掌握其正确使用方法对于编写高性能 Go 程序至关重要。

核心要点

  • 轻量级:Goroutine 比传统线程更轻量,可以创建大量实例
  • 简单启动:使用 go 关键字即可启动新的 goroutine
  • 调度器:Go 运行时自动管理 goroutine 的调度
  • 生命周期:理解 goroutine 的创建、运行和结束过程
    最佳实践
  • 避免 goroutine 泄漏:确保 goroutine 能够正常退出
  • 处理 panic:在 goroutine 中使用 defer/recover 处理异常
  • 合理数量:根据任务类型控制 goroutine 数量
  • 参数传递:注意循环变量的正确传递方式
    注意事项
  • 主 goroutine 结束时程序会立即终止
  • 未处理的 panic 会导致程序崩溃
  • 合理使用同步机制(后续章节会详细介绍)
  • 通过掌握这些概念和实践,你将能够有效地使用 Goroutine 来构建高并发的 Go 应用程序
http://www.dtcms.com/a/614678.html

相关文章:

  • 查询缓存8.0
  • 【PostgreSQL】查询所有表和视图
  • 页面布局练习
  • Cortex-M3 02-地址映射
  • 大丰做网站哪家公司好大理州城乡建设局官方网站
  • 推荐做素菜的网站电商网站开发环境怎么写
  • 企业建设网站的功能是什么意思wordpress单用户案例
  • 曼朗策划网站建设新闻源
  • 网站美工怎么做网站注册时间查询
  • 网站开发 免代码网站三网合一
  • 网站的建设服务平台昆山网站制作
  • [特殊字符]pull-aliyun:一键拉取阿里云私有镜像并简化命名
  • 1.2 学习和使用汇编语言的目的
  • 电子商务网站建设需要的语言沐风wordpress
  • SpringMVC请求参数的绑定
  • C++03 标准详解:C++98的技术修订版
  • 网站做下载wordgoogle网站推广
  • 网站虚拟机可以自己做吗查询百度关键词排名
  • Java 实战:去重与排序(HashSet+TreeSet 应用)
  • 命令行核心概念:信号、标准流与作业控制 (对比 Unix, cmd.exe, PowerShell)
  • wordpress仿站难吗央视优购物官方网站
  • 网站设计设计目的漳州做网站含博大选
  • 阿里云网站301重定向怎么做买证书网站开发工程师
  • 北京网站搜索引擎优化江苏宿迁房产网
  • 丽水市莲都区建设局网站计算机网络服务
  • WordPress做的网站源代码共享ip服务器做网站
  • 中国建设企业银行官网站深州做网站公司
  • 桐乡建设局网站高校网站建设方案
  • 青岛三吉互联网站建设公司945新开传奇网站
  • 【Chrono库】Chrono 时间舍入模块解析(round.rs)