You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从接收端关闭通道:多goroutine访问sync.Mutex引发死锁问题

嘿,我完全理解你想打破“不要从接收端关闭通道”这条规则的初衷——有时候业务场景确实会让我们想尝试这种“非常规”操作。先帮你拆解下你遇到的死锁问题,再给你几个能实现接收端优雅关闭的可行思路:

先搞清楚死锁的根源

你提到的重复锁定sync.Mutex导致死锁,大概率是这两种情况:

  • 在某个goroutine已经持有锁的情况下,又尝试再次获取同一把锁(比如锁保护的代码块里调用了另一个需要同一把锁的函数)
  • goroutine之间互相持有锁,等待对方释放

排查时可以重点检查:

  • 所有Lock()调用是否存在嵌套调用同一Mutex的Lock()
  • 确保每次Lock()后都有对应的Unlock(),哪怕是错误或return分支(最好用defer mutex.Unlock()避免遗漏)
接收端优雅关闭通道的可行方案

核心思路是引入关闭信号协调机制,让所有发送者感知到关闭意图后主动停止发送,避免直接关闭数据通道引发的panic或死锁。

方案1:用单独的关闭通知通道

这是最稳妥的方式,通过额外的done通道传递关闭信号,不触碰原数据通道的关闭逻辑:

func main() {
    dataChan := make(chan int)
    done := make(chan struct{})

    // 发送goroutine1
    go func() {
        for {
            select {
            case dataChan <- 1:
                // 正常发送数据
            case <-done:
                // 收到关闭信号,主动退出
                return
            }
        }
    }()

    // 发送goroutine2
    go func() {
        for {
            select {
            case dataChan <- 2:
                // 正常发送数据
            case <-done:
                // 收到关闭信号,主动退出
                return
            }
        }
    }()

    // 负责触发关闭的接收goroutine
    go func() {
        for num := range dataChan {
            println("收到数据:", num)
            // 假设满足某个业务条件时触发关闭
            if num == 2 {
                close(done) // 向所有发送者广播关闭信号
                return
            }
        }
    }()

    // 等待goroutine执行完成(实际项目建议用sync.WaitGroup)
    time.Sleep(time.Second)
}

这个方案里,接收端只需要关闭done通道,所有发送者通过监听done主动停止发送,完全规避了通道关闭的冲突风险。

方案2:用原子变量标记关闭状态

如果觉得额外通道麻烦,可以用sync/atomic的原子变量标记关闭状态:

func main() {
    dataChan := make(chan int)
    isClosed := atomic.Bool{}
    wg := sync.WaitGroup{}

    wg.Add(2)
    // 发送goroutine1
    go func() {
        defer wg.Done()
        for {
            if isClosed.Load() {
                return
            }
            select {
            case dataChan <- rand.Intn(10):
            default:
                // 通道满时也检查关闭状态
                if isClosed.Load() {
                    return
                }
                time.Sleep(time.Millisecond * 10)
            }
        }
    }()

    // 发送goroutine2
    go func() {
        defer wg.Done()
        for {
            if isClosed.Load() {
                return
            }
            select {
            case dataChan <- rand.Intn(10):
            default:
                if isClosed.Load() {
                    return
                }
                time.Sleep(time.Millisecond * 10)
            }
        }
    }()

    // 接收端触发关闭
    go func() {
        count := 0
        for num := range dataChan {
            println("收到数据:", num)
            count++
            if count == 5 {
                isClosed.Store(true) // 原子操作标记关闭
                return
            }
        }
    }()

    wg.Wait()
}

注意:原子变量的读写必须用原子操作,不能直接赋值或读取,否则会出现并发安全问题。

方案3:用sync.Once确保通道只关闭一次

如果一定要在接收端关闭数据通道,且能确保所有发送者已停止发送,可以用sync.Once避免重复关闭引发的panic:

func main() {
    dataChan := make(chan int)
    closeOnce := sync.Once{}
    wg := sync.WaitGroup{}

    wg.Add(2)
    // 发送goroutine1
    go func() {
        defer wg.Done()
        for {
            select {
            case dataChan <- 1:
            case <-dataChan:
                // 通道关闭后,发送会收到零值,主动退出
                return
            }
        }
    }()

    // 发送goroutine2
    go func() {
        defer wg.Done()
        for {
            select {
            case dataChan <- 2:
            case <-dataChan:
                // 通道关闭后,发送会收到零值,主动退出
                return
            }
        }
    }()

    // 接收端触发关闭
    go func() {
        for num := range dataChan {
            println("收到数据:", num)
            if num == 2 {
                closeOnce.Do(func() {
                    close(dataChan) // 确保通道只被关闭一次
                })
                return
            }
        }
    }()

    wg.Wait()
}

这个方案的前提是:接收端关闭通道后,发送者能通过监听通道状态主动退出,否则仍可能出现发送panic,所以更推荐前两种方案。

最后再提个醒

不管用哪种方案,一定要避免:

  • 在持有sync.Mutex锁的情况下执行阻塞操作(比如向无缓冲通道发送数据),这是死锁的高发场景
  • 多个goroutine同时尝试关闭同一个通道,哪怕是接收端,也要确保关闭操作的唯一性

内容的提问来源于stack exchange,提问作者skovtunenko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 07:58:56