MST

星途 面试题库

面试题:Go切片在并发场景下的应用与问题

假设有多个 goroutine 同时操作一个切片,比如有的 goroutine 向切片中添加元素,有的 goroutine 读取切片中的元素。请描述可能会出现的问题,并给出至少两种解决方案来保证数据的一致性和程序的正确性。同时,分析每种解决方案的优缺点。
43.8万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

可能出现的问题

  1. 数据竞争:多个 goroutine 同时读写切片,可能导致数据竞争,使程序出现未定义行为,结果不可预测。例如,一个 goroutine 正在读取切片时,另一个 goroutine 对切片进行了扩容或插入操作,可能导致读取到不一致的数据。
  2. 脏读:在并发环境下,一个 goroutine 可能读取到另一个 goroutine 尚未完全更新完成的切片数据,导致读取到“脏”数据。

解决方案及优缺点分析

1. 使用互斥锁(sync.Mutex)

实现方式:在对切片进行读写操作前,先获取互斥锁,操作完成后释放互斥锁。

package main

import (
    "fmt"
    "sync"
)

var (
    slice  []int
    mutex  sync.Mutex
    wg     sync.WaitGroup
)

func addElement(num int) {
    defer wg.Done()
    mutex.Lock()
    slice = append(slice, num)
    mutex.Unlock()
}

func readElement() {
    defer wg.Done()
    mutex.Lock()
    for _, v := range slice {
        fmt.Println(v)
    }
    mutex.Unlock()
}

优点

  • 简单直观,容易理解和实现。
  • 适用于各种类型的并发读写操作。 缺点
  • 性能瓶颈,当并发量高时,锁的竞争会导致性能下降,因为同一时间只有一个 goroutine 能访问切片。
  • 可能导致死锁,如果在获取锁的逻辑中出现错误,例如重复获取锁或未释放锁。

2. 使用读写锁(sync.RWMutex)

实现方式:读操作使用读锁,写操作使用写锁。多个 goroutine 可以同时获取读锁进行读取,但写操作时需要独占写锁。

package main

import (
    "fmt"
    "sync"
)

var (
    slice  []int
    rwMutex sync.RWMutex
    wg     sync.WaitGroup
)

func addElement(num int) {
    defer wg.Done()
    rwMutex.Lock()
    slice = append(slice, num)
    rwMutex.Unlock()
}

func readElement() {
    defer wg.Done()
    rwMutex.RLock()
    for _, v := range slice {
        fmt.Println(v)
    }
    rwMutex.RUnlock()
}

优点

  • 读性能较好,允许多个 goroutine 同时进行读操作,提高了并发读的效率。
  • 相比于互斥锁,在读多写少的场景下,性能提升明显。 缺点
  • 实现比互斥锁复杂一些,需要区分读锁和写锁的使用场景。
  • 写操作仍然是独占的,当写操作频繁时,会影响整体性能,因为写锁会阻塞所有读操作和其他写操作。

3. 使用 channel 进行数据传递

实现方式:通过 channel 在 goroutine 之间传递对切片的操作请求,由一个专门的 goroutine 负责实际的切片操作,避免多个 goroutine 直接操作切片。

package main

import (
    "fmt"
    "sync"
)

type Op struct {
    add  int
    read bool
}

var (
    slice  []int
    ch     = make(chan Op)
    wg     sync.WaitGroup
)

func operator() {
    for op := range ch {
        if op.read {
            for _, v := range slice {
                fmt.Println(v)
            }
        } else {
            slice = append(slice, op.add)
        }
    }
    close(ch)
}

func addElement(num int) {
    defer wg.Done()
    ch <- Op{add: num}
}

func readElement() {
    defer wg.Done()
    ch <- Op{read: true}
}

优点

  • 遵循 Go 语言“不要通过共享内存来通信,而要通过通信来共享内存”的原则,代码更符合 Go 的设计理念。
  • 减少了锁的使用,在高并发场景下性能更好,因为 channel 本身就是线程安全的。 缺点
  • 实现相对复杂,需要设计合理的 channel 通信机制和操作流程。
  • 对于复杂的切片操作,可能需要设计更复杂的消息结构和处理逻辑。