MST

星途 面试题库

面试题:Go中chan阻塞与非阻塞操作的场景应用

请描述在Go语言中,chan阻塞与非阻塞操作分别适用于哪些常见的编程场景,并举例说明。
40.0万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

阻塞操作适用场景及示例

  1. 同步协程
    • 场景描述:当需要确保一个协程完成某些任务后,另一个协程才能继续执行时,阻塞操作非常有用。例如,在一个程序中,有一个协程负责数据的计算,另一个协程负责使用计算结果进行输出,通过阻塞通道来同步这两个协程。
    • 示例代码
package main

import (
    "fmt"
)

func calculate(ch chan int) {
    result := 1 + 2
    ch <- result
}

func main() {
    ch := make(chan int)
    go calculate(ch)
    result := <-ch
    fmt.Println("计算结果:", result)
}

在这个例子中,main 函数中的 <-ch 操作是阻塞的,直到 calculate 协程向通道 ch 发送数据,这样就保证了 calculate 协程的计算任务完成后,main 函数才会继续输出结果。 2. 资源池管理

  • 场景描述:假设有一个数据库连接池,每次从连接池中获取连接和归还连接时,都希望在没有可用连接时等待,直到有连接可用。通道的阻塞操作可以很好地实现这种资源管理。
  • 示例代码
package main

import (
    "fmt"
)

func main() {
    pool := make(chan struct{}, 2) // 连接池,最多容纳2个连接
    pool <- struct{}{} // 初始化放入一个连接
    pool <- struct{}{} // 再放入一个连接

    // 获取连接
    fmt.Println("尝试获取连接")
    <-pool
    fmt.Println("获取到一个连接")

    // 再次获取连接
    fmt.Println("尝试获取第二个连接")
    <-pool
    fmt.Println("获取到第二个连接")

    // 尝试获取第三个连接,此时会阻塞
    fmt.Println("尝试获取第三个连接")
    // 这里程序会阻塞,因为没有可用连接
}

在这个代码中,通道 pool 模拟连接池,每次从通道接收数据(<-pool)就相当于获取一个连接,当没有可用连接时,接收操作会阻塞,直到有连接归还(向通道发送数据)。

非阻塞操作适用场景及示例

  1. 防止死锁
    • 场景描述:在复杂的并发程序中,可能存在多个协程互相等待资源的情况,容易导致死锁。使用非阻塞通道操作可以在某些情况下避免死锁。例如,在一个网络通信程序中,发送数据到网络可能会因为网络故障等原因阻塞,如果使用阻塞通道发送数据,可能会导致死锁。通过非阻塞发送操作,可以在发送失败时采取其他措施,如重试或记录日志。
    • 示例代码
package main

import (
    "fmt"
)

func main() {
    ch := make(chan int)
    select {
    case ch <- 1:
        fmt.Println("数据发送成功")
    default:
        fmt.Println("数据发送失败,采取其他措施,如重试或记录日志")
    }
}

在这个 select 语句中,ch <- 1 是一个向通道 ch 发送数据的操作,如果通道 ch 已满(这里没有接收方,所以可视为已满),则会执行 default 分支,避免了阻塞,也就避免了可能因阻塞发送而导致的死锁。 2. 异步任务处理

  • 场景描述:在处理大量异步任务时,不希望因为向通道发送任务而阻塞当前协程,影响整体处理效率。例如,一个 Web 服务器需要处理大量的请求,每个请求可能触发一个异步任务,使用非阻塞通道可以快速将任务放入通道,而不会阻塞处理请求的协程。
  • 示例代码
package main

import (
    "fmt"
)

func worker(ch chan int) {
    for task := range ch {
        fmt.Println("处理任务:", task)
    }
}

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

    tasks := []int{1, 2, 3, 4, 5}
    for _, task := range tasks {
        select {
        case ch <- task:
            fmt.Println("任务", task, "已提交")
        default:
            fmt.Println("任务", task, "提交失败,队列已满,可稍后重试")
        }
    }
    close(ch)
}

在这个示例中,main 函数通过 select 语句以非阻塞方式向 ch 通道发送任务。如果通道已满,任务提交失败,程序可以选择采取其他处理方式,如记录日志或稍后重试。而 worker 协程从通道接收任务并处理,这样实现了异步任务处理。