前言
主要分享协程相关的特点与基本用法
详情
名词普及
Goroutines
- Go 中的并发执行单位,类似于轻量级的线程。
- Goroutine 的调度由 Go 运行时管理,用户无需手动分配线程。
- 使用 go 关键字启动 Goroutine。
- Goroutine 是非阻塞的,可以高效地运行成千上万个 Goroutine
Channel
- Go 中用于在 Goroutine 之间通信的机制
- 支持同步和数据共享,避免了显式的锁机制
- 使用 chan 关键字创建,通过 <- 操作符发送和接收数据。
Scheduler
Go 的调度器基于 GMP 模型,调度器会将 Goroutine 分配到系统线程中执行,并通过 M 和 P 的配合高效管理并发。
- G:Goroutine
- M:系统线程(Machine)
- P:逻辑处理器(Processor)
示例
场景A:有N个任务等待消费,如何高性能的消费掉
import ("sync")
// 此处以切片代替任务
// 模拟任务投递
var task []string
for i:=0; i<100; i++ {
task = append(task, fmt.Sprintf("%s-%d", "name", i))
}
// 任务消费
// 方案1:同步排队遍历消费
// 同步消费、效率低且阻塞
func consumerA(taskList []string) {
for _, task := range taskList {
fmt.Println(task)
}
}
// 方案2,使用 Goroutinue,启用N个协程,把任务平均到每个协程中做处理
// 每个协程处理任务固定、可能浪费性能,原因:当某个协程中的任务阻塞或处理完时会挂起等待
func consumerC(taskList []string) {
// 设置协程默认数
defaultWorkers := 10
var wg sync.WaitGroup
chunkSize := (len(nameList) + defaultWorkerNum - 1) / defaultWorkerNum
for i := 0; i < defaultWorkerNum; i++ {
start := i * chunkSize
end := start + chunkSize
if end > len(nameList) {
end = len(nameList)
}
wg.Add(1)
go func(start, end int) {
defer wg.Done()
for _, name := range nameList[start:end] {
fmt.Println(name)
}
}(start, end)
}
wg.Wait()
}
// 方案3,使用 Channel
// 充分利用协程资源性能、当某个协程处理完时 任务进来会继续处理,不会等待,相比方案2 充分利用协程资源。
// 不会存在当某个协程处理任务阻塞或处理完一直等待的情况
func consumerC(taskList []string) {
// 声明 task channel 通道、并追加任务
taskChannel := make(chan string, len(taskList))
// 设置协程默认数
defaultWorkers := 10
var wg sync.WaitGroup
for i := 1; i <= defaultWorkers; i++ {
wg.Add(1)
go func () {
defer wg.Done()
for _, task := range taskChannel {
fmt.Println(task)
}
}
}
for _, task := range taskList {
taskChannel <- task
}
// 关闭channel
close(taskChannel)
// 等待Goroutinue
wg.Wait()
}
- 从以上三种方案来看方案三最为合适,合理利用空闲资源出现等待的情况较少