Golang 中的通道(channel)如何避免死锁?

在 Golang 中,通道(channel)是一种重要的用于 goroutine 间通信的机制。但是如果在使用通道时没有注意避免死锁,会导致程序卡住无法继续执行。

产生死锁的主要原因是: deux goroutines 相互等待对方发送或者接收数据,造成循环等待,程序无法继续执行。

例如:

func main() {
    ch := make(chan int)

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        ch <- 1   // 发送数据
        wg.Done()
    }()

    go func() {
        <-ch // 接收数据
        ch <- 2  // 发送数据
        wg.Done()
    }()

    wg.Wait()
}

这里启动两个 goroutine,第一个 goroutine 发送 1 到通道,第二个 goroutine 接收这个 1,然后再发送 2 到通道。

但是,第一个 goroutine 在发送 1 之后就结束了,第二个 goroutine 接收到 1 后无法再发送 2,因为没有 goroutine接收它,这就造成了死锁。

所以在使用通道实现 goroutine 间通信时,需要注意两点:

  1. 发送与接收操作必须成对出现,每次发送必须有对应的接收, vice versa。
  2. 不要让发送与接收的 goroutine 相互等待。

解决上面的例子,我们可以:

  1. 让第一个 goroutine 在发送 1 之后也接收通道数据:
``` func() {
    ch <- 1   // 发送
    <-ch      // 接收
    wg.Done()
}()
  1. 启动另一个 goroutine 专门接收第二次发送的数据:
go func() {
    <-ch  // 接收
    ch <- 2   // 发送
    wg.Done() 
}()

go func() {
    <-ch    // 接收第二次发送 
    wg.Done()
}()
  1. 使用缓冲区避免直接的发送与接收操作相互等待:
ch := make(chan int, 1) // 容量为1

ch <- 1   // 发送
go func() {   // 在发送后启动一个接收的goroutine
    <-ch 
    wg.Done()
}()  

所以总结来说,避免在使用通道时产生死锁的关键是:

  1. 发送与接收成对出现
  2. 避免直接的发送与接收操作产生相互等待
  3. 可以使用缓冲区或者中间的 goroutine 进行调度
  4. 尽量避免复杂的通信过程,这会增加死锁发生的概率