3. Go并发原语深入

Go 的并发能力不是靠线程库堆出来的,而是由运行时(runtime)统一编排。理解并发原语,首先要理解 goroutine 的调度模型:G(goroutine)、M(machine,操作系统线程)、P(processor,逻辑处理器)三者协作。G 是待执行的任务,M 是真正干活的线程,P 持有运行 G 所需的本地队列和资源。一个 M 必须绑定一个 P 才能执行 G,P 的数量由 GOMAXPROCS 决定,默认等于 CPU 逻辑核数。当某个 G 发生阻塞(如系统调用、channel 等待),运行时会把它从 M 上摘下,让 M 去执行其他 G,从而避免线程被白白占用。

调度器还维护全局队列和每个 P 的本地队列。新建的 goroutine 优先放入当前 P 的本地队列;本地队列满时,会转移一半到全局队列。当某个 P 空闲而其他 P 繁忙时,会触发 work-stealing,从别的 P 本地队列或全局队列“偷”任务。这套机制让大量 goroutine 能以较低开销并发运行,但并不意味着可以无限创建——每个 goroutine 仍有约 2KB 起始栈和调度成本,工程上应控制并发规模。

Go 并发原语协作视图 G-M-P 调度模型 G 本地队列 P 逻辑处理器 M 系统线程 阻塞时 G 被摘下,M 继续执行其他 G 空闲 P 触发 work-stealing 偷取任务 channel 类型与操作 无缓冲 chan 有缓冲 chan close 发送/接收阻塞直到配对;缓冲满/空时阻塞 关闭后接收返回零值,ok 为 false 通信 select 多路复用 case ch1 <- v case <-ch2 default 随机选择就绪分支;default 实现非阻塞 配合超时 time.After 防止永久等待 sync 核心原语 Mutex RWMutex WaitGroup Once 保护共享内存;WaitGroup 等待一组任务 竞态检测:go run -race 定位数据竞争

channel 是 Go 并发通信的核心。无缓冲 channel 的发送和接收必须同时就绪,否则阻塞;有缓冲 channel 在缓冲未满时发送不阻塞,缓冲非空时接收不阻塞。关闭 channel 后,继续接收会立即返回零值和 ok=false,而向已关闭 channel 发送会 panic。因此工程上通常由发送方关闭 channel,接收方只负责读取。判断 channel 是否关闭的标准写法是:

v, ok := <-ch
if !ok {
    // channel 已关闭且无剩余数据
}

select 用于同时等待多个 channel 操作。它会评估所有 case,若多个就绪则随机选一个,避免饥饿;若都不就绪且存在 default,则执行 default,实现非阻塞收发。典型用法是超时控制:

select {
case msg := <-ch:
    fmt.Println("收到:", msg)
case <-time.After(2 * time.Second):
    fmt.Println("超时")
}

sync 包提供的是共享内存式的同步原语。Mutex 用于互斥保护临界区,RWMutex 适合读多写少场景,WaitGroup 用于等待一组 goroutine 完成,Once 保证初始化只执行一次。使用 Mutex 时要注意锁的粒度:锁内不要做耗时 I/O,也不要嵌套获取可能造成死锁。WaitGroup 的 Add 必须在 goroutine 启动前调用,Done 用 defer 保证执行。

并发安全与竞态检测是工程落地的关键。数据竞争指两个以上 goroutine 同时访问同一变量,且至少有一个是写操作,且没有同步。Go 提供 -race 检测器:

go run -race main.go
go test -race ./...

竞态检测器会在运行时记录内存访问,发现竞争时输出冲突的读写位置和 goroutine 栈。它不能证明没有竞争,但能发现已发生的竞争。工程实践中,优先用 channel 传递数据所有权,减少共享内存;必须共享时用 Mutex 保护,并让 -race 进入 CI 流程。下面给出一个综合示例,展示 WaitGroup、Mutex 与 channel 的配合:

var (
    mu    sync.Mutex
    total int
    wg    sync.WaitGroup
)
results := make(chan int, 10)

for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(n int) {
        defer wg.Done()
        mu.Lock()
        total += n
        mu.Unlock()
        results <- n * n
    }(i)
}

wg.Wait()
close(results)
for v := range results {
    fmt.Println(v)
}
调度器让 goroutine 轻量并发,channel 负责通信,select 负责多路等待,sync 负责共享内存保护,-race 负责发现竞争。四者配合,才能写出既高效又安全的 Go 并发程序。
常见误区:认为 goroutine 越多越快。实际上 goroutine 过多会增加调度和内存开销,应结合业务用 worker pool 或信号量控制并发度;另外,向已关闭 channel 发送、重复关闭 channel 都会 panic,必须由单一发送方负责关闭。

总结来看,goroutine 调度模型决定了并发执行的底层效率,channel 和 select 提供了通信与多路复用的表达力,sync 包补齐了共享内存场景的同步手段,而竞态检测则是验证并发正确性的工程防线。理解它们各自适用的边界,比记住 API 更重要。