MST

星途 面试题库

面试题:Go 底层类型对并发编程中数据竞争的影响及应对策略

在 Go 的并发编程场景下,不同的底层类型(如结构体、指针、基本数据类型等)在多个 goroutine 同时访问时,可能引发数据竞争问题。请深入分析不同底层类型的数据竞争特点,并阐述相应的避免数据竞争的策略,包括但不限于使用 sync 包中的工具,结合实际的高并发场景代码示例进行说明。
45.2万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

不同底层类型的数据竞争特点

  1. 基本数据类型
    • 特点:多个 goroutine 同时读写基本数据类型(如 int、float、bool 等)时,可能出现数据竞争。例如,一个 goroutine 读取某个 int 变量值,同时另一个 goroutine 对其进行修改,可能导致读取到不一致的数据。
    • 示例
package main

import (
    "fmt"
    "sync"
)

var num int

func write(wg *sync.WaitGroup) {
    defer wg.Done()
    num++
}

func read(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println(num)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write(&wg)
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go read(&wg)
    }
    wg.Wait()
}

在上述代码中,多个 goroutine 同时对 num 进行读写,可能会出现数据竞争,导致输出结果不稳定。

  1. 结构体
    • 特点:当结构体包含可读写的字段,多个 goroutine 同时访问和修改这些字段时会产生数据竞争。即使结构体本身是不可变的,但如果其字段是可变的,也存在风险。
    • 示例
package main

import (
    "fmt"
    "sync"
)

type Person struct {
    Name string
    Age  int
}

var p Person

func update(wg *sync.WaitGroup) {
    defer wg.Done()
    p.Age++
    p.Name = "newName"
}

func printInfo(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Name: %s, Age: %d\n", p.Name, p.Age)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go update(&wg)
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go printInfo(&wg)
    }
    wg.Wait()
}

这里多个 goroutine 对 Person 结构体的字段进行读写,可能引发数据竞争。

  1. 指针
    • 特点:指针指向的数据可能被多个 goroutine 通过指针进行读写操作,从而导致数据竞争。尤其是当指针指向可变的数据结构时,风险更高。
    • 示例
package main

import (
    "fmt"
    "sync"
)

var ptr *int

func modifyPtr(wg *sync.WaitGroup) {
    defer wg.Done()
    if ptr == nil {
        num := 1
        ptr = &num
    } else {
        *ptr++
    }
}

func readPtr(wg *sync.WaitGroup) {
    defer wg.Done()
    if ptr != nil {
        fmt.Println(*ptr)
    }
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go modifyPtr(&wg)
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go readPtr(&wg)
    }
    wg.Wait()
}

在该示例中,多个 goroutine 通过指针 ptr 对其指向的数据进行读写,可能产生数据竞争。

避免数据竞争的策略

  1. 使用 sync.Mutex
    • 原理:互斥锁(Mutex)用于保证同一时间只有一个 goroutine 可以访问共享资源。
    • 示例
package main

import (
    "fmt"
    "sync"
)

var num int
var mu sync.Mutex

func write(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    num++
    mu.Unlock()
}

func read(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    fmt.Println(num)
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write(&wg)
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go read(&wg)
    }
    wg.Wait()
}

在上述代码中,通过 mu.Lock()mu.Unlock() 保护对 num 的读写操作,避免数据竞争。

  1. 使用 sync.RWMutex
    • 原理:读写互斥锁(RWMutex)允许多个 goroutine 同时读共享资源,但只允许一个 goroutine 写。适用于读多写少的场景。
    • 示例
package main

import (
    "fmt"
    "sync"
)

var num int
var rwmu sync.RWMutex

func write(wg *sync.WaitGroup) {
    defer wg.Done()
    rwmu.Lock()
    num++
    rwmu.Unlock()
}

func read(wg *sync.WaitGroup) {
    defer wg.Done()
    rwmu.RLock()
    fmt.Println(num)
    rwmu.RUnlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go write(&wg)
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go read(&wg)
    }
    wg.Wait()
}

这里使用 rwmu.RLock() 进行读操作,rwmu.Lock() 进行写操作,在保证写操作原子性的同时,提高读操作的并发性能。

  1. 使用 sync.Map
    • 原理sync.Map 是一个线程安全的键值对集合,内部实现了对并发读写的保护,无需用户手动加锁。
    • 示例
package main

import (
    "fmt"
    "sync"
)

var m sync.Map

func write(key string, value int, wg *sync.WaitGroup) {
    defer wg.Done()
    m.Store(key, value)
}

func read(key string, wg *sync.WaitGroup) {
    defer wg.Done()
    if val, ok := m.Load(key); ok {
        fmt.Printf("Key: %s, Value: %d\n", key, val)
    }
}

func main() {
    var wg sync.WaitGroup
    keys := []string{"k1", "k2", "k3"}
    for i, key := range keys {
        wg.Add(1)
        go write(key, i, &wg)
    }
    for _, key := range keys {
        wg.Add(1)
        go read(key, &wg)
    }
    wg.Wait()
}

sync.MapStoreLoad 方法保证了并发操作的安全性。

  1. 使用通道(Channel)
    • 原理:通过通道在 goroutine 之间传递数据,避免共享数据,从而从根本上避免数据竞争。
    • 示例
package main

import (
    "fmt"
    "sync"
)

func producer(ch chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}

func consumer(ch chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for num := range ch {
        fmt.Println(num)
    }
}

func main() {
    var wg sync.WaitGroup
    ch := make(chan int)
    wg.Add(1)
    go producer(ch, &wg)
    wg.Add(1)
    go consumer(ch, &wg)
    wg.Wait()
}

在这个例子中,通过通道 chproducerconsumer goroutine 之间传递数据,避免了共享变量带来的数据竞争。