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类型,再挑选工具

相关推荐
子兮曰2 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰2 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
一隅论数智2 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
小羊没烦恼!2 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
爱勇宝2 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
胡写代码2 天前
别再前后端各写一套表单校验了
java·后端
伞伞悦读2 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
ttwuai2 天前
Go开源后台管理系统推荐:怎么按技术栈和边界比较4个官方仓库?
golang·gin
大勇前进2 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
yuzhi_liu2 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端