GoLang的并发如何避免死锁

Go 中,并发死锁是一个常见问题,尤其是在使用 goroutinechannelmutex 时。避免死锁需要理解死锁的根源,然后采取相应策略。下面我给你系统讲解:


1️⃣ 死锁的常见原因

  1. 循环等待

    多个 goroutine 相互等待对方持有的资源,例如:

    Go 复制代码
    var mu1, mu2 sync.Mutex
    
    go func() {
        mu1.Lock()
        time.Sleep(time.Second)
        mu2.Lock()  // 等待 mu2
        mu2.Unlock()
        mu1.Unlock()
    }()
    
    go func() {
        mu2.Lock()
        time.Sleep(time.Second)
        mu1.Lock()  // 等待 mu1
        mu1.Unlock()
        mu2.Unlock()
    }()

    → 互相等待,形成死锁。

  2. 未关闭的 channel

    • 发送者发送到没有接收者的 channel。

    • 接收者等待发送者,但发送者没有启动。

      例:

    Go 复制代码
    ch := make(chan int)
    ch <- 1  // 如果没人接收,死锁
  3. 缓冲区满的 channel

    • 无缓冲 channel 或缓冲已满的 channel 都可能阻塞发送者。

    • 如果没有其他 goroutine 及时接收,可能死锁。

  4. 所有 goroutine 阻塞

    • 主 goroutine 等待子 goroutine 完成,但子 goroutine 因等待主 goroutine 或其他资源而阻塞。

    • Go 运行时会直接报 fatal error: all goroutines are asleep - deadlock!


2️⃣ 避免死锁的策略

(A) 正确使用 channel

  1. 用缓冲 channel 避免阻塞

    复制代码
    ch := make(chan int, 1) // 带缓冲
    ch <- 1  // 不会立即阻塞
  2. 确保接收者存在

    复制代码
    go func() {
        val := <-ch
        fmt.Println(val)
    }()
    ch <- 1
  3. 关闭 channel

    • 对于只读操作,发送方可以关闭 channel,让接收方通过 for range 循环退出:

      close(ch)
      for v := range ch {
      fmt.Println(v)
      }

  4. 使用 select 避免阻塞

    复制代码
    select {
    case ch <- 1:
        fmt.Println("sent")
    default:
        fmt.Println("channel full, skip")
    }

(B) 正确使用 mutex(互斥锁)

  1. 避免嵌套锁

    • 如果必须锁多个 mutex,确保所有 goroutine 按相同顺序加锁

      mu1.Lock()
      mu2.Lock()
      // ...
      mu2.Unlock()
      mu1.Unlock()

  2. 使用 defer 解锁

    复制代码
    mu.Lock()
    defer mu.Unlock()
    // 函数结束自动释放锁

(C) 使用 WaitGroup 或信号量

  • sync.WaitGroup 可以避免主 goroutine 提前退出导致死锁。

  • 例:

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
    defer wg.Done()
    fmt.Println("goroutine 1")
    }()

    go func() {
    defer wg.Done()
    fmt.Println("goroutine 2")
    }()

    wg.Wait() // 等待所有 goroutine 完成


(D) 检查所有 goroutine 的阻塞点

  • 发送/接收 channel

  • mutex 锁

  • select default 分支是否被阻塞

  • WaitGroup Done() 是否漏掉


(E) 工具辅助

  • 使用 go run -race 检测竞态条件。

  • 使用 pprofruntime.Stack() 分析死锁。

  • Go 1.18+ 有时会自动检测全局死锁并报错。


✅ 小结

避免 Go 死锁核心原则:

  1. 保证 所有 channel 的发送和接收成对存在

  2. 加锁顺序统一,避免循环等待。

  3. 合理使用缓冲 channelselect

  4. 用 WaitGroup 或 context 控制 goroutine 生命周期

  5. 工具检测 + defer 解锁,减少人为错误。


相关推荐
-Rane2 小时前
【C++】map和set
开发语言·c++
2401_873544922 小时前
分布式缓存一致性
开发语言·c++·算法
Tyooughtul2 小时前
MySQL篇 索引失效
后端
kyriewen112 小时前
为什么我的代码在测试环境跑得好好的,一到用户电脑就崩?原来凶手躲在地址栏旁边
开发语言·前端·javascript·chrome·ecmascript·html5
YMWM_2 小时前
python3中的装饰器介绍及其应用场景
java·后端·spring
sheji34162 小时前
【开题答辩全过程】以 基于Java的饮品店管理系统的实现为例,包含答辩的问题和答案
java·开发语言
暴躁小师兄数据学院2 小时前
【WEB3.0零基础转行笔记】go编程篇-第12讲:go-zero入门实战
开发语言·笔记·golang·web3·区块链
小北方城市网2 小时前
JavaScript 实战 —— 实现一个简易的 TodoList(适合前端入门 / 进阶)
开发语言·前端·javascript
向上_503582912 小时前
配置Protobuf输出Java文件或kotlin文件
android·java·开发语言·kotlin