Go开发遇见的一次Data Race

63 阅读2分钟

MRE

package main  
  
import (  
    "fmt"  
    "sync"    "time")  
  
// Store 接口模拟原代码中的 Store  
type Store interface {  
    Add(key string, value int)  
    Get(key string) int  
    GetLength() int  
}  
  
// LRUCache 模拟原代码中的 lru.Cachetype LRUCache struct {  
    cache map[string]int  
    mu    sync.RWMutex // 内部锁  
}  
  
func NewLRUCache() *LRUCache {  
    return &LRUCache{  
       cache: make(map[string]int),  
    }  
}  
  
// Add 方法没有加锁保护  
func (l *LRUCache) Add(key string, value int) {  
    l.cache[key] = value  
}  
  
// Get 方法
func (l *LRUCache) Get(key string) int {  
    return l.cache[key]  
}  
  
// GetLength 使用读锁  
func (l *LRUCache) GetLength() int {  
    l.mu.RLock()  
    defer l.mu.RUnlock()  
    return len(l.cache)  
}  
  
// MemoryCache 模拟原代码中的 MemoryCache  
type MemoryCache struct {  
    store Store  
    mu    sync.Mutex // 外部锁  
}  
  
func NewMemoryCache(store Store) *MemoryCache {  
    return &MemoryCache{  
       store: store,  
    }  
}  
  
// Set 方法加了外部锁  
func (m *MemoryCache) Set(key string, value int) {  
    m.mu.Lock()  
    defer m.mu.Unlock()  
    m.store.Add(key, value)  
}  
  
func main() {  
    lru := NewLRUCache()  
    cache := NewMemoryCache(lru)  
  
    // 模拟并发写入和读取长度  
    var wg sync.WaitGroup  
  
    // 启动多个写入 goroutine    
    for i := 0; i < 100; i++ {  
       wg.Add(1)  
       go func(i int) {  
          defer wg.Done()  
          key := fmt.Sprintf("key_%d", i)  
          cache.Set(key, i)  
       }(i)  
    }  
  
    // 启动多个读取长度的 goroutine    
    for i := 0; i < 10; i++ {  
       wg.Add(1)  
       go func() {  
          defer wg.Done()  
          for j := 0; j < 10; j++ {  
             //直接访问底层实现绕过了上层的并发保护机制  
             length := lru.GetLength()  
             fmt.Printf("当前缓存长度: %d\n", length)  
             time.Sleep(time.Millisecond)  
          }  
       }()  
    }  
  
    wg.Wait()  
}

该代码会导致以下问题

Read at 0x00c000124180 by goroutine 41:
  main.(*LRUCache).GetLength()
      main.go:42 +0xb6
  main.main.func2()
      main.go:87 +0xc4

Previous write at 0x00c000124180 by goroutine 40:
  runtime.mapassign_faststr()
      go1.18/src/runtime/map_faststr.go:203 +0x0
  main.(*LRUCache).Add()
      main.go:30 +0x5b
  main.(*MemoryCache).Set()
      main.go:61 +0xd8
  main.main.func1()
      main.go:77 +0xf1
  main.main.func3()
      main.go:78 +0x47

疑问点

  • 代码的 58 行已经加锁了,为什么还是触发了 Data Race?

仔细看代码可以发现,加锁的时候 对象为 MemoryCache,而 GetLength()的调用者则为 LRUCache,解释一下就是。 MemoryCache 相当于 青帮,58 行的加锁操作则为 ,帮派保护了帮派成员免收外部帮派欺负。相应的 GetLength()是直接帮派内部发生了内讧,从而导致问题无法解决。

  • 解决方法
// Add 方法加锁保护  
func (l *LRUCache) Add(key string, value int) {  
    l.mu.Lock()  
    l.cache[key] = value  
    l.mu.Unlock()  
}