面试题答案
一键面试检测资源竞争问题
- 使用
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
==================
- 使用
go run
检测:- 对于非测试的可执行程序,同样可以使用
-race
标志,如go run -race main.go
。
- 对于非测试的可执行程序,同样可以使用
解决资源竞争策略及场景
- 互斥锁(
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)
}
在这个场景中,生产者将任务发送到通道,消费者从通道接收任务,避免了共享资源竞争。