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

相关推荐
上海观智网络1 小时前
健身房瑜伽馆想开发小程序,上海靠谱服务商怎么选?
经验分享·笔记
swordbob1 小时前
【RabbitMQ】消息丢失的 6 大场景及解决方案
后端·rabbitmq
我认不到你1 小时前
【开源、教程】RAG全流程实现(java+完整代码):第一弹
java·开发语言·人工智能·深度学习·ai·语言模型·开源
swordbob1 小时前
Spring Bean 生命周期
开发语言·spring
程序员小羊!1 小时前
16 JAVA MySQL 8.0
java·开发语言·mysql
西凉的悲伤1 小时前
Spring Boot 中 RedisTemplate 与 StringRedisTemplate 常用 Redis API 速查
spring boot·redis·后端·redistemplate·stringredis
Dovis(誓平步青云)1 小时前
《QT学习第五篇:QSS美化界面与API绘图》
开发语言·数据库·qt·学习·时序数据库·开源智能体
无忧.芙桃1 小时前
数据结构之栈
c语言·开发语言·数据结构
wyhwust1 小时前
web应用技术--springboot01
java·开发语言