MST

星途 面试题库

面试题:Go中chan并发读写的常见问题场景及基础处理方法

在Go语言中,使用chan进行并发读写时,常常会遇到哪些问题?请举例说明,并阐述至少两种基础的处理这些问题的方法。
33.5万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试
  1. 常见问题
    • 死锁问题
      • 举例
package main

import "fmt"

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

在上述代码中,主函数尝试向无缓冲通道 ch 发送数据,但没有其他 goroutine 从该通道接收数据,这就导致了死锁。因为无缓冲通道的发送操作会阻塞,直到有接收者准备好接收数据。

  • 数据竞争问题
    • 举例
package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup
var count int

func increment(ch chan int) {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        ch <- 1
        count += <-ch
    }
}

func main() {
    ch := make(chan int)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go increment(ch)
    }
    wg.Wait()
    close(ch)
    fmt.Println("Final count:", count)
}

在这个例子中,多个 goroutine 并发地通过通道 chcount 进行读写操作。虽然通过通道传递了增量值,但由于 count 是共享变量,多个 goroutine 同时读写 count 可能会导致数据竞争,使得最终的 count 值不准确。

  1. 处理方法
    • 使用带缓冲通道避免死锁: 在上述死锁的例子中,可以修改为带缓冲通道:
package main

import "fmt"

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

这里创建了一个带缓冲为 1 的通道,发送操作不会立即阻塞,因为通道有足够的缓冲区来容纳数据,从而避免了死锁。

  • 使用互斥锁解决数据竞争
package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup
var count int
var mu sync.Mutex

func increment(ch chan int) {
    defer wg.Done()
    for i := 0; i < 1000; i++ {
        ch <- 1
        mu.Lock()
        count += <-ch
        mu.Unlock()
    }
}

func main() {
    ch := make(chan int)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go increment(ch)
    }
    wg.Wait()
    close(ch)
    fmt.Println("Final count:", count)
}

通过引入 sync.Mutex,在对共享变量 count 进行读写操作前加锁,操作完成后解锁,确保同一时间只有一个 goroutine 能够访问 count,从而避免数据竞争。

  • 使用 select 语句处理多通道场景
package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- 10
    }()

    go func() {
        time.Sleep(1 * time.Second)
        ch2 <- 20
    }()

    select {
    case val := <-ch1:
        fmt.Println("Received from ch1:", val)
    case val := <-ch2:
        fmt.Println("Received from ch2:", val)
    case <-time.After(3 * time.Second):
        fmt.Println("Timeout")
    }
}

select 语句可以在多个通道操作(接收或发送)之间进行选择。在这个例子中,它等待从 ch1ch2 接收数据,或者等待超时。如果 ch2 先接收到数据,就会打印相应信息;如果 ch1 先接收到数据,也会打印相应信息;如果在 3 秒内都没有接收到数据,就会打印超时信息。这种方式可以有效地处理多个通道的并发操作,避免死锁和其他并发问题。