MST

星途 面试题库

面试题:Go语言并发模型下,Goroutine调度与内存同步的关系及优化

在Go语言基于Goroutine的并发模型中,调度机制会影响内存同步。请分析在多核环境下,Goroutine调度时如何避免内存竞争,以及如何利用调度特性优化内存同步的性能,举例说明一些常见的优化策略和可能遇到的陷阱。
19.2万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

避免内存竞争

  1. 使用互斥锁(Mutex)
    • 当多个Goroutine需要访问共享资源时,通过互斥锁来保证同一时间只有一个Goroutine能访问该资源。例如:
    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var (
        counter int
        mu      sync.Mutex
    )
    
    func increment(wg *sync.WaitGroup) {
        defer wg.Done()
        mu.Lock()
        counter++
        mu.Unlock()
    }
    
    func main() {
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go increment(&wg)
        }
        wg.Wait()
        fmt.Println("Final counter:", counter)
    }
    
  2. 读写锁(RWMutex)
    • 适用于读多写少的场景。多个Goroutine可以同时读共享资源,但写操作时需要独占。例如:
    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var (
        data  int
        rwmu  sync.RWMutex
    )
    
    func read(wg *sync.WaitGroup) {
        defer wg.Done()
        rwmu.RLock()
        fmt.Println("Read data:", data)
        rwmu.RUnlock()
    }
    
    func write(wg *sync.WaitGroup) {
        defer wg.Done()
        rwmu.Lock()
        data++
        rwmu.Unlock()
    }
    
    func main() {
        var wg sync.WaitGroup
        for i := 0; i < 5; i++ {
            wg.Add(1)
            go read(&wg)
        }
        for i := 0; i < 2; i++ {
            wg.Add(1)
            go write(&wg)
        }
        wg.Wait()
    }
    
  3. 通道(Channel)
    • 通过通道来传递数据,避免共享内存。Goroutine之间通过通道通信,数据会安全地从一个Goroutine传递到另一个,而不会产生竞争。例如:
    package main
    
    import (
        "fmt"
    )
    
    func producer(ch chan int) {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        close(ch)
    }
    
    func consumer(ch chan int) {
        for val := range ch {
            fmt.Println("Consumed:", val)
        }
    }
    
    func main() {
        ch := make(chan int)
        go producer(ch)
        go consumer(ch)
        select {}
    }
    

利用调度特性优化内存同步性能

  1. 减少锁的粒度
    • 尽量缩小锁保护的代码块范围。比如,在一个结构体有多个字段时,如果每个字段都可以独立修改,那么可以为每个字段分别加锁,而不是为整个结构体加锁。
    package main
    
    import (
        "fmt"
        "sync"
    )
    
    type Fields struct {
        field1 int
        field2 int
        mu1    sync.Mutex
        mu2    sync.Mutex
    }
    
    func (f *Fields) updateField1() {
        f.mu1.Lock()
        f.field1++
        f.mu1.Unlock()
    }
    
    func (f *Fields) updateField2() {
        f.mu2.Lock()
        f.field2++
        f.mu2.Unlock()
    }
    
  2. 使用无锁数据结构
    • 对于一些简单的数据结构,Go标准库提供了无锁实现,如sync/atomic包。例如,原子操作可以高效地进行计数器的增减,避免锁的开销。
    package main
    
    import (
        "fmt"
        "sync"
        "sync/atomic"
    )
    
    var counter uint64
    
    func increment(wg *sync.WaitGroup) {
        defer wg.Done()
        atomic.AddUint64(&counter, 1)
    }
    
    func main() {
        var wg sync.WaitGroup
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go increment(&wg)
        }
        wg.Wait()
        fmt.Println("Final counter:", atomic.LoadUint64(&counter))
    }
    

常见陷阱

  1. 死锁
    • 当多个Goroutine互相等待对方释放锁时会发生死锁。例如,两个Goroutine分别持有一个锁,并试图获取对方持有的锁。
    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var (
        mu1 sync.Mutex
        mu2 sync.Mutex
    )
    
    func goroutine1() {
        mu1.Lock()
        fmt.Println("Goroutine 1 acquired mu1")
        mu2.Lock()
        fmt.Println("Goroutine 1 acquired mu2")
        mu2.Unlock()
        mu1.Unlock()
    }
    
    func goroutine2() {
        mu2.Lock()
        fmt.Println("Goroutine 2 acquired mu2")
        mu1.Lock()
        fmt.Println("Goroutine 2 acquired mu1")
        mu1.Unlock()
        mu2.Unlock()
    }
    
    func main() {
        go goroutine1()
        go goroutine2()
        select {}
    }
    
    • 这个程序会发生死锁,因为goroutine1获取mu1后等待mu2,而goroutine2获取mu2后等待mu1
  2. 误判通道关闭
    • 在使用for... range遍历通道时,如果在不恰当的时候关闭通道,可能会导致数据丢失或程序逻辑错误。例如,在生产者还未完全发送完数据时就关闭通道。
    package main
    
    import (
        "fmt"
    )
    
    func producer(ch chan int) {
        for i := 0; i < 5; i++ {
            ch <- i
            if i == 2 {
                close(ch) // 错误关闭,数据还未完全发送
            }
        }
    }
    
    func consumer(ch chan int) {
        for val := range ch {
            fmt.Println("Consumed:", val)
        }
    }
    
    func main() {
        ch := make(chan int)
        go producer(ch)
        go consumer(ch)
        select {}
    }
    
    • 在这个例子中,生产者在发送2个数据后就关闭了通道,导致剩余数据无法发送给消费者。