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

相关推荐
蜗牛互联网7 分钟前
Java 17 调用视觉 Responses API:JSON 约束与人工复核路由
java·人工智能·后端
PC2005-cloud17 分钟前
Mysql学习笔记:InnoDB Cluster 集群从零搭建实战,基于 Docker 实现高可用读写分离架构
笔记·学习·mysql
外收内放21 分钟前
Python基础语法练习题(69)
开发语言·python
lojox25 分钟前
深入理解 C++ STL:vector 底层实现、迭代器失效与 list 特性
开发语言·c++
iotxiaohu28 分钟前
用 cflow 查看 C 函数调用关系
c语言·开发语言·cflow
zhang-ge28 分钟前
python包和项目管理器uv在ubuntu下的安装与使用
开发语言·python·uv
小陈phd29 分钟前
深入理解agent学习笔记(七)——动态提示词与Skills
笔记·学习
我命由我1234530 分钟前
田野笔记极简理解
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
小陈phd1 小时前
深入理解agent学习笔记(六)——提示工程-优化系统提示词
笔记·学习
高频因子挖掘机1 小时前
股票分红送转后价格“断层”:前复权、后复权和不复权该怎么选?
后端·github·api