sync.Map
是一个并发安全的 map 实现。 sync.Map
中原始 map 的类型实际上是 map[any]*entry
。
当我们调用Map.LoadOrStore
并且条目存在时,entry.tryLoadOrStore
被调用,以下是该函数的代码
func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {
p := e.p.Load()
if p == expunged {
return nil, false, false
}
if p != nil {
return *p, true, true
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if e.p.CompareAndSwap(nil, &ic) {
return i, false, true
}
p = e.p.Load()
if p == expunged {
return nil, false, false
}
if p != nil {
return *p, true, true
}
}
}
这是另一个函数trySwap
,当我们调用Swap
或Store
时,也会调用这个函数。
func (e *entry) trySwap(i *any) (*any, bool) {
for {
p := e.p.Load()
if p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, i) {
return p, true
}
}
}
tryLoadOrStore
可以像 trySwap
一样仅基于其逻辑来实现,但事实并非如此。我的问题是:既然它们的逻辑相似,为什么它们的实现方式不同?
当我尝试理解时,我认为这是因为参数类型的差异,如果I
是*any
,则不需要进行复制,因为它是已经是一个指针,我们不需要关心逃逸分析。但似乎没有什么特殊原因需要从外部调用者那里获取地址。
if e, ok := read.m[key]; ok {
if v, ok := e.trySwap(&value); ok {
if v == nil {
return nil, false
}
return *v, true
}
}
然后我不知道为什么这两个函数(以及其他函数)以不同的方式实现。
最佳答案
首先,a quote来自 sync.Map
的原作者 Bryan Mills:
sync.Map
is pretty gnarly to begin with!
sync.Map
中的代码对逃逸分析非常敏感,并且实现由基准测试驱动。
让我们深入研究一下提交历史记录。它应该可以帮助我们理解为什么它们以不同的方式实现。
初步实现于CL 37342 :
- first patchset 中的实现草案相似之处:
func (e *entry) tryStore(i interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(&i)) {
return true
}
}
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, clean bool) {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&i)) {
return i, false, true
}
}
}
- 接口(interface)复制技巧已添加到patchset 3中的两个实现中:
// Copy the interface to make this method more amenable to escape analysis:
// if we hit the "load" path or the entry is expunged, we shouldn't bother
// heap-allocating.
ic := i
(*entry).tryStore
被修改为接受 patchset 5 中的指针:
我找不到对此更改的评论。这很可能是逃逸分析和基准测试的结果。
func (e *entry) tryStore(i *interface{}) bool {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
for {
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
}
}
func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
// Copy the interface after the first load to make this method more amenable
// to escape analysis: if we hit the "load" path or the entry is expunged, we
// shouldn't bother heap-allocating.
ic := i
for {
if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
return i, false, true
}
p = atomic.LoadPointer(&e.p)
if p == expunged {
return nil, false, false
}
if p != nil {
return *(*interface{})(p), true, true
}
}
}
(*entry).tryStore
在 CL 137441 中得到简化:
此更改可防止这种逃逸到堆的情况:
sync/map.go:178:26: &e.p escapes to heap
sync/map.go:178:26: from &e.p (passed to call[argument escapes]) at
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
CL 399094 中的 (*entry).tryStore
已重命名为 (*entry).trySwap
:
func (e *entry) trySwap(i *any) (*any, bool) {
for {
p := e.p.Load()
if p == expunged {
return nil, false
}
if e.p.CompareAndSwap(p, i) {
return p, true
}
}
}
仅此而已。
注意:其他一些小 CL 未列出,例如 CL 426074将实现切换为使用atomic.Pointer。
关于go - 在 Gosync.Map 中,为什么这部分实现不一致或者我误解了什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/76186937/