Go-Goroutine 基础与调度机制

1. 什么是 Goroutine

Goroutine 是 Go 语言中的一种轻量级线程实现。相比传统线程,Goroutine
的开销更小,因此可以在同一进程内运行成千上万个 Goroutine,使得 Go 能够更高效地处理并发任务。

在 Go 中,Goroutine 以独立的协程形式执行,通过 go 关键字启动。所有的 Goroutine 在同一地址空间中运行,可以直接共享数据,但需要小心并发冲突。


2. Goroutine 的基本用法

要启动一个 Goroutine,我们只需在函数调用前添加 go 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"time"
)

func sayHello() {
fmt.Println("Hello from Goroutine")
}

func main() {
go sayHello()
fmt.Println("Hello from main")
time.Sleep(time.Second) // 保证主线程不会过早退出
}

在上述代码中,我们通过 go sayHello() 启动了一个新的 Goroutine。注意,main 函数中的 fmt.Println("Hello from main")
sayHello 函数是并发执行的。为了避免 main 函数过早退出,我们使用 time.Sleep 暂停一秒,以便 sayHello 有机会运行。

使用匿名函数启动 Goroutine

我们也可以直接在 Goroutine 中使用匿名函数:

1
2
3
go func() {
fmt.Println("Running in an anonymous Goroutine")
}()

这种方式特别适合一些一次性任务或轻量任务的并发执行。


3. Goroutine 的调度机制

Go 语言中有一个 GMP 模型来调度 Goroutine,其中:

  • G 代表 Goroutine
  • M 代表 Machine,对应一个内核线程
  • P 代表 Processor,用于管理 GM 的绑定

GMP 模型简介

  • G(Goroutine):代表需要执行的任务,包含了任务函数和运行时栈。
  • M(Machine):代表系统的内核线程,负责实际的执行。
  • P(Processor):代表逻辑处理器,负责调度 Goroutine

P 通过调度 Goroutine 队列,分发任务给 M 去执行。G 绑定 P 后再由 M 执行,如果 G 阻塞,M
可以切换去执行其他 Goroutine,从而达到非阻塞的效果。

Goroutine 的协作式调度

Go 的调度器采用协作式调度,Goroutine 在适当时刻会主动让出 CPU,调度器则把控制权交给下一个 Goroutine
。例如,在 Goroutine 中使用 time.Sleepruntime.Gosched() 会主动触发调度,避免长时间占用 CPU。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"runtime"
)

func main() {
go func() {
for i := 0; i < 5; i++ {
fmt.Println("Goroutine:", i)
runtime.Gosched() // 主动让出 CPU
}
}()

for i := 0; i < 5; i++ {
fmt.Println("Main:", i)
}
}

在这里,我们使用 runtime.Gosched() 函数让出 CPU,让调度器去执行其他的 Goroutine


4. 实际应用示例

并发任务的执行

在很多场景下,Goroutine 可以帮助我们执行多个并发任务,比如以下例子中启动了多个 Goroutine 来执行计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"sync"
)

func calculate(wg *sync.WaitGroup, id int) {
defer wg.Done() // 减少计数器
fmt.Printf("Task %d started
", id)
}

func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // 增加计数器
go calculate(&wg, i)
}
wg.Wait() // 等待所有 Goroutine 完成
fmt.Println("All tasks completed")
}

在这个例子中,我们使用 sync.WaitGroup 等待所有 Goroutine 完成,确保在 main 函数结束之前所有任务都完成。


5. 总结与最佳实践

  • 不要阻塞 Goroutine:在 Goroutine 中避免使用阻塞的操作,可以使用通道(Channel)来协调并发任务。
  • 合理使用 WaitGroup:在并发任务中,sync.WaitGroup 可以帮助我们控制 Goroutine 的结束,避免 main 函数过早退出。
  • 关注内存泄漏:如果创建了大量的 Goroutine,一定要小心避免内存泄漏,尽量确保所有 Goroutine 都能正确结束。
  • 熟悉 GMP 模型:理解 Go 的调度机制和 GMP 模型有助于编写高效的并发程序。
  • 不要依赖调度顺序Goroutine 的调度顺序是不可控的,编写并发程序时要确保结果不依赖于执行的顺序。