三类常见并发bug

Bug 类型 一句话描述
Data Race 多人同时抢一块数据,结果乱套
Deadlock 互相等对方,大家一起卡死
Goroutine 泄漏 开了一堆 goroutine,但忘了关,内存越跑越大

一、Data Race

典型问题代码:

var n int // 全局变量,初始值为0

func add() {
    for i := 0; i < 1000; i++ {
        n++ // 对全局变量n自增1
    }
}

func main() {
    go add() // 启动第一个goroutine执行add()
    go add() // 启动第二个goroutine执行add()
    time.Sleep(100 * time.Millisecond) // 主线程休眠,等待两个goroutine执行完
    fmt.Println("n =", n) // 打印最终结果
}
  • 会出现的问题:按理说会输出2000(两个goroutine每个都写入1000),但是每次运行程序,输出的值都不一样且达不到2000,可能是1878也可能是1923...
  • 为什么会出现这样的问题:两个 goroutine 同时执行 n++,互相覆盖对方写入的操作
  • n++实际覆盖三步操作:读,改,写(不符合原子性),因此在写的过程中两个goroutine会互相干扰

这个案例就体现了data race的典型问题:结果不确定

如何看到底哪行代码出现问题,以及哪两个goroutine出现了冲突:

使用:-race

go run -race main.go

race的局限性:

能发现 不能发现
并发读写同一块内存 死锁
goroutine 泄漏
逻辑顺序错误(没有写冲突)
如何解决data race问题:加Mutex以及WaitGroup

重点:用 mu.Lock()mu.Unlock() 保护 n++

var (
    n  int
    mu sync.Mutex  // 变量声明:互斥锁
    wg sync.WaitGroup  // 变量声明:WaitGroup
)

func add() {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        mu.Lock()  // 加互斥锁
        n++
        mu.Unlock()  // 解锁
    }
}

func main() {
    wg.Add(2)  // 要等两个goroutine
    go add()
    go add()
    wg.Wait()  // 阻塞主线程,直到WaitGroup的计数器减到0
    fmt.Println("n =", n)  // 稳定输出 2000
}

二、死锁Dead Lock

典型问题代码:

func main() {
    ch := make(chan int)
    ch <- 1           // 发送:等有人接收
    fmt.Println(<-ch) // 接收:等有人发送
}
  • 会出现的问题:死锁

  • 为什么会出现这个问题:无缓冲channel需要一个goroutine发送,一个goroutine同时接收;而这里只有一个goroutine,会卡在 ch <- 1这一步
  • 无缓冲 channel:发送方和接收方必须同时准备好
修复代码:
func main() {
    ch := make(chan int)

    go func() {
        ch <- 1   // 另一个人发
    }()
    
    fmt.Println(<-ch)  // main 收
}

三、Goroutine泄露

典型问题代码:

func leak() {
    ch := make(chan int)
    
    go func() {
        val := <-ch   // 永远等不到数据
        fmt.Println(val)
    }()
    
    // 函数返回,ch 没人再往里发
    // 但 goroutine 还在等,永远不会退出
}
  • 问题:只有 val := 在收集channel的数据,但是没有人发给他,导致goroutine一直在等待
  • 主要的问题:无缓冲通道的接收操作没有对应的发送操作
症状 说明
内存越来越大 堆积的 goroutine 占内存
没有报错 不像死锁那样显式崩溃
goroutine 数量只增不减 最隐蔽的问题
如何发现 Goroutine 泄露

修复代码:加推出信号
func noLeak() {
    ch := make(chan int)
    quit := make(chan struct{})   // 退出信号 channel
    
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-quit:               // 收到退出信号就走
            return
        }
    }()
    
    close(quit)   // 通知 goroutine 退出
}

四、总结

实用习惯:

  • 测试时始终带 -racego test -race -count=1 ./...
  • 给每个等待点加日志,知道「谁在等谁
  • 怀疑死锁:按 Ctrl+\ 打印所有 goroutine 当前的调用
  • 不要用 time.Sleep 假装同步
  • 先让程序跑正确,再谈性能

先判断bug类型,再挑选工具

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐