MST

星途 面试题库

面试题:Go chan的多路复用及死锁场景分析

请描述select语句如何实现对多个chan的多路复用。假设存在多个goroutine通过chan进行通信,分析在什么情况下会产生死锁,并且如何避免这种死锁情况。
46.0万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

使用select语句实现对多个chan的多路复用

在Go语言中,select语句用于在多个通信操作(如从通道接收数据或向通道发送数据)之间进行选择。其语法结构如下:

select {
case <-chan1:
    // 当chan1有数据可读时执行这里
case chan2 <- value:
    // 当chan2可以接收数据时执行这里
default:
    // 当上面的case都不满足时执行这里(可选)
}

在一个select语句中,Go运行时会阻塞,直到其中一个case可以继续执行(即某个通道可以进行读或写操作)。如果多个case都准备好,Go会随机选择一个执行。

死锁情况分析

  1. 所有通道阻塞:当select语句中所有case涉及的通道操作都无法立即进行(即没有通道准备好读或写),并且没有default分支时,就会发生死锁。例如:
package main

import "fmt"

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

    select {
    case <-ch1:
        fmt.Println("Received from ch1")
    case <-ch2:
        fmt.Println("Received from ch2")
    }
}

在上述代码中,ch1ch2都没有数据可读,且没有default分支,所以select语句会一直阻塞,导致死锁。 2. 循环中通道操作不匹配:在循环中,如果通道的发送和接收操作不匹配,也可能导致死锁。例如:

package main

import "fmt"

func main() {
    ch := make(chan int)
    for i := 0; i < 2; i++ {
        select {
        case ch <- i:
            fmt.Printf("Sent %d to ch\n", i)
        }
    }
    // 这里没有从ch读取数据的操作,ch会一直阻塞在发送操作上,最终导致死锁
}

避免死锁的方法

  1. 添加default分支:在select语句中添加default分支,可以避免因所有通道阻塞而导致的死锁。default分支会在所有case都无法立即执行时立即执行。例如:
package main

import "fmt"

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

    select {
    case <-ch1:
        fmt.Println("Received from ch1")
    case <-ch2:
        fmt.Println("Received from ch2")
    default:
        fmt.Println("No channel is ready")
    }
}
  1. 确保通道操作匹配:在循环中使用通道时,要确保通道的发送和接收操作在数量和逻辑上匹配。例如:
package main

import "fmt"

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

    go func() {
        for i := 0; i < 2; i++ {
            ch <- i
        }
        close(ch)
    }()

    for val := range ch {
        fmt.Printf("Received %d from ch\n", val)
    }
}

在上述代码中,通过在一个goroutine中向通道发送数据,并在主goroutine中使用for... range从通道接收数据,直到通道关闭,确保了通道操作的匹配,避免了死锁。同时,及时关闭通道也很重要,它可以通知接收方不再有数据发送。