面试题答案
一键面试可能出现的数据竞争点分析
- 共享数据的读写:多个goroutine同时读写共享数据,例如一个全局变量或结构体中的字段,如果没有适当的同步机制,就会产生数据竞争。例如:
var sharedVar int
func read() {
_ = sharedVar
}
func write() {
sharedVar = 1
}
这里如果read
和write
函数在不同的goroutine中并发执行,就可能出现数据竞争。
2. 嵌套同步原语:在使用嵌套的同步原语时,如果顺序不当,可能导致死锁或隐藏的数据竞争。比如在Mutex
嵌套使用中,如果内层锁没有及时释放,外层锁再次获取时就会出现问题。
var mu1, mu2 sync.Mutex
func f1() {
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
// 业务逻辑
}
func f2() {
mu2.Lock()
defer mu2.Unlock()
mu1.Lock()
defer mu1.Unlock()
// 业务逻辑
}
如果f1
和f2
在不同goroutine中并发执行,就可能出现死锁。
避免数据竞争和优化策略
- 合理使用同步机制
- Mutex:使用
sync.Mutex
来保护共享数据。在读写共享数据前获取锁,操作完成后释放锁。
- Mutex:使用
var sharedVar int
var mu sync.Mutex
func read() {
mu.Lock()
defer mu.Unlock()
_ = sharedVar
}
func write() {
mu.Lock()
defer mu.Unlock()
sharedVar = 1
}
- **RWMutex**:对于读多写少的场景,使用`sync.RWMutex`。多个读操作可以同时进行,但写操作需要独占锁。
var sharedVar int
var rwmu sync.RWMutex
func read() {
rwmu.RLock()
defer rwmu.RUnlock()
_ = sharedVar
}
func write() {
rwmu.Lock()
defer rwmu.Unlock()
sharedVar = 1
}
- **Channel**:使用channel进行数据传递,避免共享数据。通过channel在goroutine之间传递数据,从而减少对共享数据的直接操作。
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
func consumer(ch <-chan int) {
for num := range ch {
// 处理num
}
}
- 优化内存访问模式
- 减少共享数据:尽量减少全局变量和共享数据的使用,将数据限制在单个goroutine内处理。如果必须共享,尽量通过channel传递数据。
- 局部化数据:将数据分配到各个goroutine的局部变量中,减少对共享数据的依赖。例如,在处理任务时,每个goroutine可以有自己的任务副本进行处理。
- 使用原子操作:对于简单的共享数据类型(如
int
、int64
等),可以使用atomic
包中的原子操作。原子操作在硬件层面保证了操作的原子性,避免了数据竞争。
import "sync/atomic"
var sharedInt64 int64
func increment() {
atomic.AddInt64(&sharedInt64, 1)
}
func get() int64 {
return atomic.LoadInt64(&sharedInt64)
}
- 避免嵌套同步原语:尽量简化同步原语的嵌套使用,如果确实需要嵌套,要仔细设计锁的获取和释放顺序,避免死锁。可以考虑使用其他方式来实现相同的逻辑,例如使用
select
语句结合channel来实现更灵活的同步。 - 测试和工具:使用Go语言内置的
go test -race
工具来检测数据竞争。在开发过程中,经常运行这个命令可以及时发现潜在的数据竞争问题,并进行修复。