生活智库网
白蓝主题五 · 清爽阅读
首页  > 亲子教育

Go并发编程常见问题 实用操作步骤与避坑指南

Go代码的时候,很多人一开始都觉得goroutine真香,一个go关键字就起一个协程,轻松实现并发。可实际用起来才发现,看似简单的背后藏着不少坑。

共享变量引发的数据竞争

就像家里两个孩子同时抢一个玩具,程序里多个goroutine同时读写同一个变量,结果谁也说不准。比如下面这段代码:

var count = 0

func main() {
for i := 0; i < 10; i++ {
go func() {
for j := 0; j < 100000; j++ {
count++
}
}()
}
time.Sleep(time.Second)
fmt.Println(count)
}

你可能以为输出是100万,但实际往往不是。因为count++不是原子操作,多个goroutine同时改,数据就乱了。这时候得靠互斥锁来协调:

var mu sync.Mutex

go func() {
for j := 0; j < 100000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}

goroutine泄露:忘了收场

有时候启了一个goroutine去做事,结果任务早就结束了,但它还在那里挂着,像忘了关的水龙头。比如从channel读数据,但没人往里写:

ch := make(chan int)
go func() {
val := <-ch
fmt.Println(val)
}()
// 忘了close(ch)或写入数据
time.Sleep(time.Second)

这个goroutine永远卡在那,内存就一点点被吃掉。解决办法是设置超时或者确保channel有写入和关闭。

select的随机性让人懵

select用来处理多个channel操作,但它的执行顺序是随机的。比如:

ch1, ch2 := make(chan int), make(chan int)
go func() { ch1 <- 1 }()
go func() { ch2 <- 2 }()

select {
case <-ch1:
fmt.Println("来自ch1")
case <-ch2:
fmt.Println("来自ch2")
}

你不能指望每次都先打印ch1,因为它可能是ch2。这就像两个孩子同时喊你,你本能地回应那个声音更明显的,而不是按顺序来的。

WaitGroup使用不当导致死锁

WaitGroup常用来等所有goroutine结束,但如果Add和Done不匹配,程序就会卡住。比如:

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 做点事
}()
}
wg.Wait()

看着没问题,但如果Add放在了goroutine里面,就可能还没加上去,Wait就已经开始了,结果就是部分任务没被追踪,提前退出。必须保证Add在goroutine启动前完成。

过度依赖并发反而拖慢程序

不是所有任务都适合并发。就像让一个五岁孩子同时背三首诗,结果哪首都没记住。小任务开太多goroutine,调度开销反而比收益大。该串行时就别硬上并发。