1. Go 语言切片在并发编程中的陷阱
- 资源竞争:多个 goroutine 同时读写切片时,可能会发生数据竞争,导致结果不可预测。例如,一个 goroutine 正在写入切片,另一个 goroutine 同时读取切片,可能读到未完全写入的数据。
- 动态扩容问题:切片在容量不足时会自动扩容,并发环境下扩容操作可能导致数据不一致。比如多个 goroutine 同时触发扩容,会造成复杂的竞争情况。
2. 设计思路
- 互斥锁:使用
sync.Mutex
来保护对切片的读写操作。在每次读写切片前获取锁,操作完成后释放锁,以此避免资源竞争。
- 读写锁:如果读操作远多于写操作,可以考虑使用
sync.RWMutex
。读操作时使用读锁,允许多个 goroutine 同时读;写操作时使用写锁,保证只有一个 goroutine 能写,防止写时读和写时写的竞争。
- 性能优化:尽量减少锁的持有时间,将复杂操作分解,在持有锁期间只进行必要的切片操作。可以考虑使用缓存,减少对实际切片的直接操作频率。
3. 代码示例(使用互斥锁)
package main
import (
"fmt"
"sync"
)
type SafeSlice struct {
data []int
mutex sync.Mutex
}
func (s *SafeSlice) Append(num int) {
s.mutex.Lock()
s.data = append(s.data, num)
s.mutex.Unlock()
}
func (s *SafeSlice) Get(index int) (int, bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if index < 0 || index >= len(s.data) {
return 0, false
}
return s.data[index], true
}
func main() {
safeSlice := SafeSlice{}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int) {
defer wg.Done()
safeSlice.Append(num)
}(i)
}
go func() {
wg.Wait()
for i := 0; i < 10; i++ {
value, ok := safeSlice.Get(i)
if ok {
fmt.Println("Index", i, "Value", value)
}
}
}()
select {}
}
4. 代码示例(使用读写锁)
package main
import (
"fmt"
"sync"
)
type SafeSliceWithRWLock struct {
data []int
rwMutex sync.RWMutex
}
func (s *SafeSliceWithRWLock) Append(num int) {
s.rwMutex.Lock()
s.data = append(s.data, num)
s.rwMutex.Unlock()
}
func (s *SafeSliceWithRWLock) Get(index int) (int, bool) {
s.rwMutex.RLock()
defer s.rwMutex.RUnlock()
if index < 0 || index >= len(s.data) {
return 0, false
}
return s.data[index], true
}
func main() {
safeSlice := SafeSliceWithRWLock{}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int) {
defer wg.Done()
safeSlice.Append(num)
}(i)
}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
value, ok := safeSlice.Get(index)
if ok {
fmt.Println("Index", index, "Value", value)
}
}(i)
}
wg.Wait()
}