深色模式
Go语言并发基础
概述
Go 一提并发,很多人第一反应就是 channel。这不算错,但只盯着 channel 看,会把并发问题看窄。Go 的并发模型真正重要的不是某一个语法点,而是它给了几种不同的协作方式:启动并发执行、传递消息、等待完成、保护共享状态。
这一篇不把所有 channel 细节一次讲完,而是先把并发基础地图立住。先知道各个工具在解决什么问题,后面看 channel、锁、上下文取消时才不会全都挤成一团。
并发不等于并行
先把两个词分开:
- 并发:多个任务在时间上交错推进
- 并行:多个任务在同一时刻真正同时执行
Go 的 goroutine 让并发变得很容易表达,但是否并行执行,还取决于运行时调度和可用 CPU。
goroutine 是并发执行的入口
启动一个 goroutine 很简单:
go
go doWork()或者:
go
go func() {
fmt.Println("hello")
}()它的意义是把一个函数调用交给运行时并发调度,而不是让当前调用方阻塞等它做完。
并发里首先要解决的是“怎么等它结束”
只会启动 goroutine 不够,还得知道怎么等结果。
最基础的思路有两类:
- 用
channel传完成信号或结果 - 用
sync.WaitGroup等一组任务结束
例如:
go
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()这里 WaitGroup 解决的不是通信,而是“等一批任务收尾”。
channel 解决的是通信和同步
channel 的核心价值,是让 goroutine 之间用消息传递来协作。
go
ch := make(chan int)
go func() {
ch <- 42
}()
v := <-ch
fmt.Println(v)这段代码同时做了两件事:
- 传递了数据
42 - 让发送和接收在时序上完成同步
select 用来协调多个通信分支
当 goroutine 同时关心多个 channel 事件时,select 就会出现:
go
select {
case v := <-ch1:
fmt.Println(v)
case ch2 <- 1:
fmt.Println("sent")
default:
fmt.Println("no-op")
}它的作用不是替代 switch,而是在多个通道操作中选一个当前可执行的分支。
不要把并发问题全交给 channel
Go 的并发工具本来就不只 channel 一种。例如:
- 任务编排、结果传递:
channel - 等待一组 goroutine 结束:
sync.WaitGroup - 保护共享状态:
sync.Mutex - 只读多写少的场景:
sync.RWMutex
哪个工具更合适,取决于问题本身,不取决于口号。
锁解决的是共享可变状态
如果多个 goroutine 共同读写一块共享数据,锁往往更直接:
go
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}这里如果硬改成 channel,不一定更清楚。并发设计里,最值得避免的不是锁,而是对问题类型判断错误。
一条够用的判断线
写 Go 并发代码时,可以先问自己:
- 我是要启动并发任务,还是要组织它们之间的关系。
- 我需要传递消息,还是只是等待结束。
- 我是在协调多个执行流,还是在保护一块共享内存。
回答清楚这三个问题,工具往往就自己浮出来了。
