goroutine使用
goroutine是Go语言从语言层面提供的支持,使用更少的栈内存,是一种比线程更轻量级的并发实现.因此go相比较其他语言可以表现出更好的并发性能,能够在程序中高效地执行多个任务。
开启协程
通过go关键字
func sayHello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
go sayHello() // 启动一个新的 Goroutine
time.Sleep(time.Second) // 等待 Goroutine 执行
}
Goroutine 是异步执行的,主 Goroutine(main 函数)不会等待其他 Goroutine 自动完成,因此像下面这种情况,循环走的比协程开启得快,打印的都是11111......
for i := 1; i <= 10; i++ {
go func(){
fmt.Println(i)
}()
}
互斥锁
sync.Mutex提供了两种基本方法:
Lock():加锁,当锁已经被其他 Goroutine 持有时,会阻塞当前 Goroutine。Unlock():解锁,释放锁资源,允许其他 Goroutine 获得锁 如果某些 Goroutine 只需要读取数据,可以使用读写锁sync.RWMutex。它允许多个 Goroutine 同时读取,但写操作仍是独占的。
sync.WaitGroup
用于等待一组 Goroutine 完成执行,允许主 Goroutine等待多个子 Goroutine 执行完成后再继续执行。
基本方法
-
Add(delta int):增加或减少等待计数。- 参数
delta:可以为正(增加计数)或负(减少计数)。
- 参数
-
Done():减少等待计数,等价于Add(-1)。 -
Wait():阻塞调用它的 Goroutine,直到等待计数变为 0。
使用流程
- 创建一个
sync.WaitGroup实例。 - 在启动每个 Goroutine 前调用
Add(1)增加计数。 - 在每个 Goroutine 完成任务后调用
Done(),减少计数。 - 主 Goroutine 调用
Wait(),等待计数变为 0,继续执行。Wait()会阻塞调用它的 Goroutine,通常用于主 Goroutine 等待子 Goroutine。
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Goroutine 完成时减少计数
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // 模拟任务耗时
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)
}
wg.Wait() // 等待所有 Goroutine 完成
}
可能存在问题
- 如果调用了
Add(1)却忘记调用Done(),Wait()会永久阻塞。因此要使用defer wg.Done(),确保在 Goroutine 中任务完成后减少计数。 - 需要使用
defer,避免忘记调用Done()。
Goroutine 调度
Go 的调度器基于 GMP 模型,实现了高效的协程调度机制,runtime层面的实现,是完全由 Go 语言本身实现的一套调度系统——go scheduler。调度器会采用本地队列、任务窃取和抢占式调度等优化机制。 其中,GMP 模型由以下三个主要部分组成:
- G(Goroutine) :
- 表示 Goroutine,Go 中的轻量级线程。
- 每个 Goroutine 都包含需要执行的函数、堆栈信息等。
- 调度器会把 G 分配到 P 中排队等待执行。
- M(Machine) :
- 表示操作系统的线程。
- 每个 M 绑定一个内核线程,负责实际执行代码。
- M 通过绑定 P 来获取 Goroutine。
- P(Processor) :
- 表示逻辑处理器,负责调度 Goroutine。
- 每个 P 维护一个本地队列,存储需要运行的 Goroutine。
- P 的数量由环境变量
GOMAXPROCS控制。