【问题标题】:Does this singleton instance implementation has a race condition?这个单例实例实现是否有竞争条件?
【发布时间】:2021-02-23 12:48:57
【问题描述】:

有人告诉我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
}

【问题讨论】:

  • 仅供参考,您所做的事情有一个名称:它称为“双重检查锁定”。在大多数计算机只有一个 CPU 内核的时代,这曾经是一种常见的模式,但当多核计算机出现时,它就变成了一种反模式。您可能会在较旧的教科书和/或程序中找到它的示例,但如果是这样,这些程序/教科书已经过时了。 (但是,当然你不会找到任何旧的go 程序!)

标签: go concurrency singleton data-race


【解决方案1】:

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

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

func main() {
    go GetMemCache()
    GetMemCache()
}

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

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

一个简单的修复方法是删除不同步的读取:

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

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

    return memCacheInstance
}

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

var (
    memCacheInstance *MemCache
    memCacheOnce     sync.Once
)

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

    return memCacheInstance
}

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

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

func GetMemCache() *MemCache {
    return memCacheInstance
}

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

【讨论】:

  • @AlexanderTrakhimenok 有一场比赛,所以不太好。有比赛的地方没什么好说的。不要诱惑魔鬼。见Is it safe to read a function pointer concurrently without a lock?
  • @AlexanderTrakhimenok “我看不出这里有什么问题”。那么这篇文章是给你的,请阅读:Benign Data Races: What Could Possibly Go Wrong?
  • 也许建议在这里使用golang.org/src/sync/once.go 是有意义的,因此无需锁定读取。顺便说一句,我很欣赏你的回答,并在 StackOverflow 上关注你。
  • @AlexanderTrakhimenok 好主意,在答案中添加了更多细节。
  • @AlexanderTrakhimenok:这绝对不正确。即使您可以确定底层硬件符合您的期望(例如,x86 上的简单字大小的加载和存储,但请参阅“良性数据竞赛”),当使用 Go(或任何高于机器代码的语言)编程时,您是编程到 Go 内存模型,而不是硬件。这不是“Go Way”的问题,它只是遵循语言中规定的规则的问题。 FYI 竞争检测器是 C/C++ ThreadSanitizer 运行时库的实现,使用相同的底层规则来检测数据竞争。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-03
  • 1970-01-01
  • 2010-11-21
相关资源
最近更新 更多