MST

星途 面试题库

面试题:Go语言Goroutine的并发控制与资源竞争处理

假设你有多个Goroutine同时访问和修改共享资源,描述如何使用Go语言提供的机制(如sync包中的工具)来控制并发访问,避免资源竞争问题。请举例说明,并阐述不同机制在这种场景下的优缺点。
42.8万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试
  1. 使用sync.Mutex(互斥锁)
    • 控制并发访问方式:通过sync.MutexLockUnlock方法来保护共享资源。在访问共享资源前调用Lock方法,访问结束后调用Unlock方法。如果有多个Goroutine同时尝试获取锁,只有一个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 value:", counter)
}
  • 优缺点
    • 优点:简单易用,适用于大多数需要保护共享资源的场景。对于读写操作比例没有特殊要求的场景都很适用。
    • 缺点:性能问题,在高并发下,如果锁竞争激烈,会导致很多Goroutine阻塞,降低程序的并发性能。而且如果Unlock调用被遗漏,会导致死锁。
  1. 使用sync.RWMutex(读写互斥锁)
    • 控制并发访问方式:适用于读多写少的场景。允许多个Goroutine同时读共享资源,但写操作时需要独占锁。读操作调用RLock,写操作调用Lock,相应的结束后调用RUnlockUnlock
    • 示例代码
package main

import (
    "fmt"
    "sync"
)

var (
    data    int
    rwMutex sync.RWMutex
)

func read(wg *sync.WaitGroup) {
    defer wg.Done()
    rwMutex.RLock()
    fmt.Println("Read value:", data)
    rwMutex.RUnlock()
}

func write(wg *sync.WaitGroup) {
    defer wg.Done()
    rwMutex.Lock()
    data++
    fmt.Println("Write value:", data)
    rwMutex.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()
}
  • 优缺点
    • 优点:在读写操作比例不均衡,读操作远多于写操作的场景下,性能比sync.Mutex好,因为读操作可以并发进行,减少了等待时间。
    • 缺点:相比sync.Mutex,实现更复杂。而且如果写操作频繁,读操作也会因为等待写锁的释放而阻塞,导致性能下降。
  1. 使用sync.Cond(条件变量)
    • 控制并发访问方式:结合sync.Mutex使用,用于在某些条件满足时通知Goroutine。一个Goroutine可以等待某个条件变量被通知,其他Goroutine可以在满足条件时发送通知。
    • 示例代码
package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    ready bool
    mu    sync.Mutex
    cond  sync.Cond
)

func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    for!ready {
        cond.Wait()
    }
    fmt.Println("Worker is working")
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go worker(&wg)

    time.Sleep(2 * time.Second)
    mu.Lock()
    ready = true
    cond.Broadcast()
    mu.Unlock()
    wg.Wait()
}
  • 优缺点
    • 优点:适用于需要根据某些条件来决定是否进行下一步操作的场景,能够更灵活地控制Goroutine的执行顺序。
    • 缺点:使用起来相对复杂,需要小心处理条件变量和锁的关系,否则容易出现死锁或竞态条件。
  1. 使用sync.Map
    • 控制并发访问方式:这是Go 1.9引入的并发安全的map。无需手动加锁,内部实现了并发控制机制,可以直接在多个Goroutine中安全地读写。
    • 示例代码
package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            key := fmt.Sprintf("key%d", n)
            m.Store(key, n)
            value, ok := m.Load(key)
            if ok {
                fmt.Printf("Key: %s, Value: %d\n", key, value)
            }
        }(i)
    }
    wg.Wait()
}
  • 优缺点
    • 优点:使用简单,无需手动管理锁,适用于需要在并发环境下使用map的场景,性能在高并发下表现良好。
    • 缺点:它不支持遍历(虽然可以通过Range方法进行类似遍历的操作,但与普通map的遍历行为有差异),也不支持获取map的长度。如果需要这些功能,可能不太适用。