MST

星途 面试题库

面试题:Go语言中如何避免channel死锁?

假设你有一个复杂的并发程序,其中涉及多个goroutine通过channel进行通信。请详细说明在这种场景下,可能导致死锁的常见情况有哪些,以及如何通过合理设计goroutine和channel的使用来避免死锁。例如,给出一些简单的代码示例说明死锁场景和对应的修正方案。
16.1万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

可能导致死锁的常见情况

  1. 双向通信死锁:两个或多个goroutine互相等待对方通过channel发送数据,形成循环依赖。例如:
package main

import "fmt"

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        ch1 <- 1
        fmt.Println(<-ch2)
    }()

    go func() {
        ch2 <- 2
        fmt.Println(<-ch1)
    }()

    select {}
}

在上述代码中,两个goroutine互相等待对方发送数据,导致死锁。

  1. 无缓冲channel读写不匹配:只在一个goroutine中对无缓冲channel进行写操作,而没有其他goroutine同时进行读操作;或者只进行读操作,而没有写操作。例如:
package main

import "fmt"

func main() {
    ch := make(chan int)
    ch <- 1
    fmt.Println(<-ch)
}

这里在主goroutine中向无缓冲channel ch 写入数据,但没有其他goroutine来读取,导致死锁。

  1. 关闭channel不当:在多个goroutine共享的channel关闭时,如果处理不当,可能导致死锁。例如,一个goroutine关闭channel后,另一个goroutine还在尝试向已关闭的channel写数据。

  2. 资源竞争与死锁:当多个goroutine竞争共享资源,并且获取资源的顺序不一致时,可能导致死锁。比如多个goroutine同时获取多个互斥锁,获取顺序不同就可能造成死锁。

避免死锁的方法

  1. 合理设计通信逻辑:确保goroutine之间的通信不会形成循环依赖。例如,对于上述双向通信死锁的代码,可以修改为:
package main

import "fmt"

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        ch1 <- 1
        fmt.Println(<-ch1)
    }()

    go func() {
        data := <-ch1
        ch1 <- data + 1
    }()

    select {}
}

这里通过调整通信逻辑,避免了互相等待的情况。

  1. 使用缓冲channel:在适当情况下,使用有缓冲的channel可以减少死锁风险。例如:
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 1
    fmt.Println(<-ch)
}

有缓冲的channel允许在没有读操作时先写入一定数量的数据,避免了无缓冲channel读写不匹配导致的死锁。

  1. 正确关闭channel:确保只有一个goroutine负责关闭channel,并且在关闭前检查是否所有需要的数据都已发送。例如:
package main

import "fmt"

func main() {
    ch := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch)
    }()

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

这里通过for... range循环来读取channel数据,直到channel关闭,避免了对已关闭channel的无效操作。

  1. 使用锁和资源管理:对于共享资源竞争问题,使用互斥锁(sync.Mutex)等机制来保护共享资源,并确保所有goroutine以一致的顺序获取锁。例如:
package main

import (
    "fmt"
    "sync"
)

var mu sync.Mutex
var data int

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        mu.Lock()
        data++
        mu.Unlock()
    }()

    go func() {
        defer wg.Done()
        mu.Lock()
        fmt.Println(data)
        mu.Unlock()
    }()

    wg.Wait()
}

通过互斥锁保证了对共享变量data的安全访问,避免了因资源竞争导致的死锁。