规避 Go 中的常见并发 bug
在Understanding Real-World Concurrency Bugs in Go这篇论文中,几名研究人员分析了常见的Go并发bug,并在最流行的几个Go开源项目中进行了验证。本文梳理了论文中提到的常见的bug并给出解决方法的分析。 论文中对bugs进行了分类,分为阻塞式和非阻塞式两种: 阻塞式:goroutine发生阻塞无法继续执行(例如死锁) 非阻塞式:不会阻塞执行,但存在潜在的数据冲突(例如并发写) 阻塞式bug 阻塞式bug发生的根因有两种,一种是共享内存(例如卡在了意图保护共享内存的锁操作上),一种是消息传递(比如等待chan)。同时研究发现共享内存和消息传递导致的bug数量不想上下,但是共享这种方法的使用量比消息传递使用的更频繁,所以也得出了共享内存方式更不容易导致bug的结论。 读写锁优先级导致的死锁 在Go中的写锁优先级高于读锁优先级,假设一个goroutine(goroutine A)连续获取两次读锁,而另一个goroutine(goroutine B)在gouroutine A两次获取读锁中间获取了写锁,就会导致死锁的发生。论文中没有针对这个bug给出示例代码,我写了一个简单的代码示意一下。 func gouroutine1() { m.RLock() m.RLock() } func gouroutine2() { m.WLock() } f1和f2都在goroutine中执行,当f1执行完第一个l.RLock()语句后,假设这时f2的m.WLock执行,由于写锁是排它的,WLock本身被f1的第一个m.RLock()阻塞,写锁操作本身又会阻塞f1中的第二个m.RLock WaitGroup误用导致的死锁 这种情况就是比较典型的WaitGroup的误用了,提前执行group.Wait()会导致部分group.Done()无法执行到,进而导致程序被阻塞。 var group sync.WaitGroup group.Add(len(pm.plugins)) for _, p := range pm.plugins { go func(p *plugin) { defer group.Done() } group.Wait() // blocked } // group.Wait() should be here for循环内的group.Wait()执行到的时候,循环内的部分goroutine还没有被创建出来,其中的group.Done()也就永远没法执行到,所以会导致永远阻塞在这一句,正确的写法是将group.Wait()移到for循环外。 Channel的误用 Channel是go支持并发的一个非常重要的特性,Channel虽然在很多场景下非常解决问题,但是误用也是不容易发现的。 func goroutine1() { m.Lock() ch <- request // blocked m.Unlock() } func goroutine2() { for { m.Lock() // 阻塞 m.Unlock() request <- ch } } 这段代码的业务语义是goroutine1会通过ch接收goroutine2发送的消息,但是当goroutine1执行到ch <- request时候会阻塞并等待ch,此时由于goroutine1没有释放锁,goroutine2的m.Lock()也会阻塞,形成死锁。 ...