Go语言分布式计算(并发Debug)

三类常见并发bug

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

一、Data Race

典型问题代码:

Go 复制代码
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++

Go 复制代码
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

典型问题代码:

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

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

三、Goroutine泄露

典型问题代码:

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

四、总结

实用习惯:

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

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

相关推荐
万少6 分钟前
万少的博客 - 技术分享与解决方案
前端·javascript·后端
咖啡八杯17 分钟前
GoF设计模式——备忘录模式
java·后端·spring·设计模式
苍何26 分钟前
腾讯再放大招,企微 Agent 大圆开启内测
后端
ethantan37 分钟前
一篇讲解AI Agent 组成:像人一样思考的智能体
人工智能·后端·程序员
apocelipes2 小时前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
Cosolar2 小时前
vLLM 生产级部署完全指南
人工智能·后端·架构
IT_陈寒3 小时前
垃圾回收器选错了,我的Java服务内存炸了
前端·人工智能·后端
用户8356290780514 小时前
使用 Python 在 PDF 中创建与管理书签
后端·python
Nturmoils4 小时前
字段太多看不全,ksql 的展开模式和输出控制怎么用
数据库·后端
大志说编程4 小时前
Agent面试真题06: 十分钟带你快速掌握Agent记忆管理高频面试题(附详细答案)
后端·面试·ai编程