MST

星途 面试题库

面试题:Go Channel性能优化之避免死锁与饥饿

在使用Go Channel进行并发编程时,死锁和饥饿是常见的性能与正确性问题。请详细描述如何在复杂的并发场景下,通过合理的Channel使用策略,避免死锁与饥饿现象的发生,同时给出一个容易产生死锁或饥饿的代码示例,并说明如何修改。
33.2万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

避免死锁策略

  1. 确保发送和接收平衡
    • 在使用channel时,要保证发送和接收操作的数量匹配。如果一个channel只有发送操作而没有接收操作,或者只有接收操作而没有发送操作,就容易导致死锁。例如,在一个生产者 - 消费者模型中,生产者向channel发送数据,消费者从channel接收数据,两者的速率要合理匹配。
    • 可以使用缓冲channel来缓解瞬间的不平衡。比如ch := make(chan int, 10),这样在一定程度上可以容纳更多的数据,避免生产者因channel满而阻塞。
  2. 使用select语句
    • select语句可以监听多个channel的操作,当其中一个channel准备好时,就执行相应的分支。这在处理多个channel交互时非常有用。例如:
    select {
    case data := <-ch1:
        // 处理从ch1接收的数据
    case ch2 <- data:
        // 向ch2发送数据
    default:
        // 当没有任何channel准备好时执行
    }
    
    • default分支可以防止select语句永远阻塞,从而避免死锁。
  3. 正确的关闭channel
    • 发送方负责关闭channel,接收方通过ok值来判断channel是否关闭。例如:
    data, ok := <-ch
    if!ok {
        // channel已关闭,处理关闭逻辑
    }
    
    • 不要在接收方关闭channel,否则可能导致数据丢失或死锁。

避免饥饿策略

  1. 公平调度
    • 尽量使用公平的调度策略。例如,在处理多个channelselect语句中,避免让某些channel总是优先被选中。如果一个channelselect语句中的位置靠前,并且总是有数据准备好,那么其他channel可能会一直得不到执行机会,从而产生饥饿。
    • 可以通过随机化select语句中channel的顺序来实现更公平的调度。
  2. 合理设置优先级
    • 如果确实需要对某些channel设置优先级,可以通过在接收数据后进行额外的处理来平衡。比如,给高优先级channel的数据加上标记,在处理时优先处理,但同时也要定期处理低优先级channel的数据。

死锁代码示例

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int)
    ch <- 1 // 向无缓冲channel发送数据,但没有接收操作
    fmt.Println(<-ch)
}

死锁原因

上述代码中,ch是一个无缓冲channel,向其发送数据时会阻塞,直到有接收操作。但这里没有任何接收操作就直接发送数据,所以会导致死锁。

修改方法

package main

import (
    "fmt"
)

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

这里通过启动一个新的goroutine来发送数据,并且在发送完成后关闭channel,主goroutinechannel接收数据,避免了死锁。

饥饿代码示例

package main

import (
    "fmt"
)

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

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

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

饥饿原因

ch1会不断地向channel发送数据,在select语句中,只要ch1有数据,就会优先执行ch1的接收分支,ch2几乎没有机会被选中,从而导致ch2产生饥饿。

修改方法

package main

import (
    "fmt"
    "math/rand"
    "time"
)

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

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

    rand.Seed(time.Now().UnixNano())
    for {
        channels := []chan int{ch1, ch2}
        index := rand.Intn(len(channels))
        select {
        case data := <-channels[index]:
            fmt.Printf("Received from ch%d: %d\n", index+1, data)
        }
    }
}

这里通过随机化select语句中channel的选择顺序,使得ch1ch2都有机会被选中,避免了ch2的饥饿。