题目
实现并发安全的计数器并避免竞态条件
信息
- 类型:问答
- 难度:⭐⭐
考点
Goroutine同步,互斥锁使用,WaitGroup使用
快速回答
实现并发安全计数器的关键点:
- 使用
sync.Mutex或sync.RWMutex保护共享状态 - 通过
sync.WaitGroup等待所有Goroutine完成 - 避免在锁内执行耗时操作
- 使用
defer确保锁的释放
问题场景
多个Goroutine并发操作同一个计数器时,需要保证计数操作的原子性和最终结果的正确性。
原理说明
在Go中,当多个Goroutine同时访问共享资源时会出现竞态条件(race condition)。解决方式:
- 互斥锁(Mutex):保证同一时间只有一个Goroutine能访问临界区
- WaitGroup:主Goroutine等待所有工作Goroutine完成
- 原子操作:对于简单操作可使用
sync/atomic包
代码示例
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := SafeCounter{}
var wg sync.WaitGroup
// 启动100个Goroutine
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Final count:", counter.Value()) // 正确输出100
}最佳实践
- 使用
defer释放锁,避免忘记解锁 - 锁的粒度要适中:过大会降低并发性,过小可能遗漏保护
- 考虑使用
RWMutex当读多写少时提高性能 - 避免在锁内执行I/O等耗时操作
常见错误
- 忘记释放锁导致死锁
- 复制已使用的
sync.Mutex(值传递导致锁失效) - 未使用WaitGroup导致主Goroutine提前退出
- 错误地认为
++操作是原子的(实际是读-改-写三步操作)
扩展知识
- 原子操作:对于简单计数器可使用
atomic.AddInt32()替代锁 - ErrGroup:需要错误传播时使用
golang.org/x/sync/errgroup - Channel同步:可通过channel实现计数器(但不如Mutex直观)
- 竞态检测:编译/运行时添加
-race标志检测竞态条件