这个单例实例实现有竞争条件吗?

And*_*Cui 0 concurrency singleton go data-race

有人告诉我memCacheInstance有竞争条件,但go run -race不知道。

代码:

type MemCache struct {
    data []string
}

var memCacheInstance *MemCache
var memCacheCreateMutex sync.Mutex

func GetMemCache() *MemCache {
    if memCacheInstance == nil {
        memCacheCreateMutex.Lock()
        defer memCacheCreateMutex.Unlock()

        if memCacheInstance == nil {
            memCacheInstance = &MemCache{
                data: make([]string, 0),
            }
        }
    }
    return memCacheInstance
}
Run Code Online (Sandbox Code Playgroud)

icz*_*cza 5

围棋比赛检测器不会检测到每场比赛,但是当它检测到时,它总是一个积极的案例。您必须编写代码来模拟不雅行为。

如果GetMemCache()从多个 goroutine 调用,您的示例会发生数据竞争。这个简单的例子触发了竞争检测器:

func main() {
    go GetMemCache()
    GetMemCache()
}
Run Code Online (Sandbox Code Playgroud)

运行它go run -race .,输出是:

==================
WARNING: DATA RACE
Read at 0x000000526ac0 by goroutine 6:
  main.GetMemCache()
      /home/icza/gows/src/play/play.go:13 +0x64

Previous write at 0x000000526ac0 by main goroutine:
  main.GetMemCache()
      /home/icza/gows/src/play/play.go:18 +0x17e
  main.main()
      /home/icza/gows/src/play/play.go:28 +0x49

Goroutine 6 (running) created at:
  main.main()
      /home/icza/gows/src/play/play.go:27 +0x44
==================
Found 1 data race(s)
exit status 66
Run Code Online (Sandbox Code Playgroud)

它有一个竞争,因为memCacheInstance变量的第一次读取没有锁定,没有同步。对变量的所有并发访问必须同步,其中至少一个访问是写。

一个简单的修复是删除非同步读取:

func GetMemCache() *MemCache {
    memCacheCreateMutex.Lock()
    defer memCacheCreateMutex.Unlock()

    if memCacheInstance == nil {
        memCacheInstance = &MemCache{
            data: make([]string, 0),
        }
    }

    return memCacheInstance
}
Run Code Online (Sandbox Code Playgroud)

还要注意,要以并发安全的方式只执行一次代码,有sync.Once. 你可以这样使用它:

var (
    memCacheInstance *MemCache
    memCacheOnce     sync.Once
)

func GetMemCache() *MemCache {
    memCacheOnce.Do(func() {
        memCacheInstance = &MemCache{
            data: make([]string, 0),
        }
    })

    return memCacheInstance
}
Run Code Online (Sandbox Code Playgroud)

另请注意,如果您“立即”初始化变量(在声明时或在包init()函数中),则不需要同步(因为包初始化在单个 goroutine 中运行):

var memCacheInstance = &MemCache{
    data: make([]string, 0),
}

func GetMemCache() *MemCache {
    return memCacheInstance
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您也可以选择导出变量,然后就不需要GetMemCache().

  • @AlexanderTrakhimenok:这绝对是不正确的。即使您可以确定底层硬件符合您的期望(例如,在 x86 上进行简单字大小的加载和存储,但请参阅“良性数据竞争”),当使用 Go(或任何高于机器代码的语言)进行编程时,您对 Go 内存模型进行编程,而不是对硬件进行编程。这不是“Go Way”的问题,而只是遵循语言中规定的规则的问题。仅供参考,竞争检测器是 C/C++ ThreadSanitizer 运行时库的实现,使用相同的底层规则来检测数据竞争。 (3认同)
  • @AlexanderTrakhimenok 有一场比赛,所以不好。凡是有比赛的地方都不好。不要诱惑魔鬼。请参阅[在没有锁的情况下并发读取函数指针是否安全?](/sf/ask/2898455101/ -锁/41407827#41407827) (2认同)
  • 也许建议在这里使用 https://golang.org/src/sync/once.go 是有意义的,这样就不需要锁定读取。顺便说一句,我很欣赏你的回答,并在 StackOverflow 上关注你。 (2认同)