MST

星途 面试题库

面试题:Go并发任务执行性能调优之资源竞争排查

在Go并发程序中,由于多个goroutine同时访问共享资源可能会导致资源竞争问题,严重影响程序性能。请阐述如何使用Go内置的工具检测这类资源竞争问题,在定位到问题后,通常有哪些策略来解决资源竞争以优化并发任务的执行性能,结合具体场景说明。
25.3万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

检测资源竞争问题

  1. 使用go test检测
    • 在测试文件(例如xxx_test.go)中编写测试用例,对可能存在资源竞争的代码进行并发调用测试。
    • 使用-race标志运行测试,如go test -race。例如:
package main

import (
    "sync"
    "testing"
)

var sharedVar int

func concurrentAccess(wg *sync.WaitGroup) {
    sharedVar++
    wg.Done()
}

func TestRaceCondition(t *testing.T) {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go concurrentAccess(&wg)
    }
    wg.Wait()
}

运行go test -race,如果存在资源竞争,会输出类似如下信息:

==================
WARNING: DATA RACE
Write at 0x00c000018088 by goroutine 7:
  main.concurrentAccess()
      /path/to/your/file.go:10 +0x38

Previous read at 0x00c000018088 by goroutine 6:
  main.concurrentAccess()
      /path/to/your/file.go:10 +0x28

Goroutine 7 (running) created at:
  main.TestRaceCondition()
      /path/to/your/file.go:17 +0x98

Goroutine 6 (finished) created at:
  main.TestRaceCondition()
      /path/to/your/file.go:17 +0x98
==================
  1. 使用go run检测
    • 对于非测试的可执行程序,同样可以使用-race标志,如go run -race main.go

解决资源竞争策略及场景

  1. 互斥锁(sync.Mutex
    • 策略:使用互斥锁来保护共享资源,确保同一时间只有一个goroutine可以访问共享资源。
    • 场景:例如在银行转账操作中,假设有一个共享的账户余额变量。
package main

import (
    "fmt"
    "sync"
)

type Account struct {
    balance int
    mutex   sync.Mutex
}

func (a *Account) Withdraw(amount int) {
    a.mutex.Lock()
    defer a.mutex.Unlock()
    if a.balance >= amount {
        a.balance -= amount
    }
}

func main() {
    var wg sync.WaitGroup
    account := Account{balance: 1000}
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            account.Withdraw(100)
        }()
    }
    wg.Wait()
    fmt.Println("Final balance:", account.balance)
}

在这个场景中,sync.Mutex保证了balance变量在并发操作时不会出现资源竞争。 2. 读写锁(sync.RWMutex

  • 策略:当读操作远多于写操作时,使用读写锁。读操作可以并发进行,而写操作会独占资源,防止其他读写操作。
  • 场景:例如一个缓存系统,大量的goroutine可能读取缓存数据,但只有少数会更新缓存。
package main

import (
    "fmt"
    "sync"
)

type Cache struct {
    data    map[string]string
    rwMutex sync.RWMutex
}

func (c *Cache) Read(key string) string {
    c.rwMutex.RLock()
    defer c.rwMutex.RUnlock()
    return c.data[key]
}

func (c *Cache) Write(key, value string) {
    c.rwMutex.Lock()
    defer c.rwMutex.Unlock()
    if c.data == nil {
        c.data = make(map[string]string)
    }
    c.data[key] = value
}

func main() {
    cache := Cache{}
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key%d", id)
            cache.Write(key, fmt.Sprintf("value%d", id))
        }(i)
    }
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("key%d", id%5)
            value := cache.Read(key)
            fmt.Printf("Read key %s, value %s\n", key, value)
        }(i)
    }
    wg.Wait()
}

这里读写锁在写操作少而读操作多的场景下,既保证了数据一致性,又提高了并发性能。 3. 通道(channel

  • 策略:通过通道来传递数据,避免共享资源的直接竞争。每个goroutine处理自己接收到的数据,而不是共享内存中的数据。
  • 场景:例如一个任务分发系统,有多个任务生产者和任务消费者。
package main

import (
    "fmt"
    "sync"
)

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

func consumer(tasks <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for task := range tasks {
        fmt.Printf("Consumed task %d\n", task)
    }
}

func main() {
    var wg sync.WaitGroup
    tasks := make(chan int)
    wg.Add(1)
    go producer(tasks, &wg)
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go consumer(tasks, &wg)
    }
    wg.Wait()
    close(tasks)
}

在这个场景中,生产者将任务发送到通道,消费者从通道接收任务,避免了共享资源竞争。