
本文详解go并发编程中的同步机制,对比基于channel的手动同步与sync.waitgroup的标准化方案,揭示竞态条件成因,并提供安全、可维护的goroutine协作范式。
在Go中,“线程同步”这一说法其实并不准确——Go不直接暴露操作系统线程,而是通过轻量级的goroutine和channel构建并发模型。真正的同步目标是:确保主goroutine能可靠等待所有工作goroutine完成后再读取共享状态。你提供的两个示例恰好展现了同步缺失带来的典型问题。
第一个版本使用done chan bool看似可行,但存在严重缺陷:Goroutine2函数调用时遗漏了done参数(原代码中go Goroutine2(i_chan)未传done),导致其无法发送完成信号,程序必然死锁。即使修复为go Goroutine2(i_chan, done),逻辑上也隐含风险——两个goroutine通过同一通道i_chan串行修改整数,虽避免了数据竞争,但性能极低(本质是伪并发),且done通道容量为2的设计依赖调用顺序,健壮性不足。
而第二个无同步版本的问题根源在于缺乏执行完成的确定性通知机制。time.Sleep(100 * time.Millisecond)只是经验性“碰运气”:它既不能保证goroutine已启动,也无法确认它们是否真正执行完毕。在不同CPU负载、GC时机或调度器行为下,结果完全不可预测——这正是典型的竞态条件(Race Condition):程序行为依赖于不可控的调度时序。
✅ 正确、现代的同步方式应使用标准库的 sync.WaitGroup:
立即学习“go语言免费学习笔记(深入)”;
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func Goroutine1(iChan chan int, wg *sync.WaitGroup) {
defer wg.Done() // 确保无论何种退出都计数减一
for x := 0; x < 1_000_000; x++ {
i := <-iChan
i++
iChan <- i
}
}
func Goroutine2(iChan chan int, wg *sync.WaitGroup) {
defer wg.Done()
for x := 0; x < 1_000_000; x++ {
i := <-iChan
i--
iChan <- i
}
}
func main() {
iChan := make(chan int, 1)
iChan <- 0
var wg sync.WaitGroup
wg.Add(2) // 预期等待2个goroutine
runtime.GOMAXPROCS(runtime.NumCPU())
go Goroutine1(iChan, &wg)
go Goroutine2(iChan, &wg)
wg.Wait() // 阻塞直到计数归零
fmt.Printf("This is the value of i: %d\n", <-iChan)
}关键优势解析:
- WaitGroup 语义清晰:Add()声明任务数,Done()标记完成,Wait()阻塞等待——无需手动管理通道、类型或缓冲区;
- 线程安全:内部使用原子操作,无竞态风险;
- 资源友好:比通道更轻量,无额外goroutine或内存分配开销;
- 错误防御强:defer wg.Done()确保panic时仍能正确计数。
⚠️ 注意事项:
- 切勿在WaitGroup实例上进行拷贝传递(必须传指针);
- Add()应在go语句前调用,避免因goroutine启动延迟导致Wait()提前返回;
- 若需超时控制,可结合context.WithTimeout与WaitGroup(通过闭包或额外channel实现)。
总结:Go的并发同步不是“锁住线程”,而是协调goroutine生命周期。优先选用sync.WaitGroup处理“等待完成”场景;对共享数据访问,应优先通过channel传递所有权,其次才考虑sync.Mutex等显式锁。摒弃time.Sleep这类非确定性等待,是写出可靠并发Go程序的第一步。










