Golang 协程(Goroutine)
简介
Golang(Go 语言)以其强大的并发模型而著称,而协程(Goroutine)是 Go 并发编程的核心特性之一。Goroutine 是一种轻量级线程,由 Go 运行时管理,能够高效地执行并发任务,相比传统线程更占用更少的资源。
本文将深入探讨 Goroutine 的基础概念、使用方法、常见实践以及最佳实践,帮助开发者更好地理解和应用 Go 语言的并发编程能力。
目录
- 什么是 Goroutine?
- Goroutine 的基本使用
- Goroutine 的调度机制
- Goroutine 与通道(Channel)通信
- Goroutine 的常见实践
- Goroutine 的最佳实践
- 小结
- 参考资料
1. 什么是 Goroutine?
Goroutine 是 Go 语言提供的轻量级并发单元。它类似于线程(Thread),但更加高效。Go 运行时会根据需要创建和管理 Goroutine,而不会像操作系统线程那样占用大量资源。
Goroutine 的特点:
- 轻量级:一个 Goroutine 仅占用数 KB 内存,而一个线程通常占用数 MB。
- 用户态调度:Go 运行时自己管理 Goroutine 的调度,而非依赖操作系统的线程调度。
- 动态扩展:Go 运行时会根据 CPU 核心数合理地调度 Goroutine,从而提高并发执行效率。
2. Goroutine 的基本使用
在 Go 语言中,使用 go
关键字即可启动一个新的 Goroutine。例如:
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
go sayHello() // 启动一个 Goroutine 执行 sayHello()
fmt.Println("Main function execution")
time.Sleep(time.Second) // 确保 Goroutine 有时间执行
}
代码解析:
go sayHello()
启动了一个新的 Goroutine 运行sayHello
函数。main
函数继续执行,不会等待sayHello
运行完毕。time.Sleep(time.Second)
让main
程序暂停 1 秒,确保sayHello
运行完成(否则main
结束后程序直接退出,Goroutine 可能还未执行完)。
⚠️ 注意: main
函数是主 Goroutine,所有 Goroutine 必须在 main
结束前执行,否则会被直接终止。
3. Goroutine 的调度机制
Go 运行时包含一个高效的 Goroutine 调度器,负责在多个 操作系统线程(M) 上调度 Goroutine(G) 的执行。Go 运行时使用 GPM 模型 进行调度:
- G(Goroutine):表示 Go 协程。
- M(Thread):表示操作系统线程。
- P(Processor):表示 Goroutine 运行所需的 CPU 处理器。
Goroutine 的调度策略:
- 抢占式调度:避免 Goroutine 长时间占用 CPU。
- 多 P 支持:运行时默认使用
GOMAXPROCS
设定的 CPU 核心数(默认等于 CPU 核心数)。 - 自动调度:Go 运行时自动决定 Goroutine 何时执行,无需手动管理。
可以通过 runtime.GOMAXPROCS(n)
设置并发执行的 CPU 核心数。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("CPU 核心数:", runtime.NumCPU()) // 获取 CPU 核心数
fmt.Println("默认 GOMAXPROCS:", runtime.GOMAXPROCS(0)) // 获取当前 GOMAXPROCS 值
runtime.GOMAXPROCS(2) // 设置为 2 个核心
}
4. Goroutine 与通道(Channel)通信
在 Go 语言中,协程之间的通信通常使用 通道(Channel) 进行数据交换。Channel 提供了一种安全的方式在 Goroutine 之间共享数据。
Channel 的基本使用
package main
import "fmt"
func worker(ch chan string) {
ch <- "Goroutine Message"
}
func main() {
ch := make(chan string) // 创建一个无缓冲通道
go worker(ch) // 启动 Goroutine
msg := <-ch // 从通道接收数据
fmt.Println(msg)
}
通道的特点:
make(chan T)
创建一个通道(无缓冲)。<-
操作符用于发送或接收数据(阻塞)。- 发送方
ch <- "data"
发送数据。 - 接收方
msg := <-ch
接收数据。
5. Goroutine 的常见实践
1. 使用 sync.WaitGroup
等待所有 Goroutine 完成
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 标记 Goroutine 完成
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加计数
go worker(i, &wg) // 启动 Goroutine
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All workers done")
}
2. 使用 sync.Mutex
进行 Goroutine 资源同步
package main
import (
"fmt"
"sync"
)
var counter int
var mu sync.Mutex
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Final Counter:", counter)
}
6. Goroutine 的最佳实践
- 避免过度创建 Goroutine
- 虽然 Goroutine 轻量,但仍然消耗内存,过多可能导致性能问题。
- 优先使用 Channel 而非共享内存
- Go 提倡 “不要通过共享内存来通信,而是通过通信来共享内存”。
- 使用
sync.WaitGroup
进行 Goroutine 同步- 确保所有 Goroutine 在
main
退出前执行完毕。
- 确保所有 Goroutine 在
- 使用
context
控制 Goroutine 生命周期context.WithCancel
、context.WithTimeout
可用于超时控制 Goroutine。
示例:
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker stopped")
return
default:
fmt.Println("Worker running")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(2 * time.Second)
cancel() // 取消 Goroutine
time.Sleep(1 * time.Second)
}
7. 小结
- Goroutine 是 Go 语言的核心并发特性,使用
go
关键字创建。 - Go 运行时使用 GPM 调度模型 高效管理 Goroutine。
Channel
是 Goroutine 之间安全的通信方式。sync.WaitGroup
、sync.Mutex
和context
可用于协程同步和控制。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)