深入探索sync.Map(含源码解析)

为什么有sync.Map

在日常编码中,map 是我们常用的数据结构之一,因为他能在o(1) 的时间复杂度内通过一个键值获取到对应的值, 这是非常高效的.然而,虽然 Go 提供了内置的 map,但它并不适用于多线程环境,因为在并发读写时可能会引发数据竞争,从而导致不安全的行为。
为了解决这一问题,Go 提供了 sync.Map,它在实现上做了特别设计,允许我们在多线程环境下安全、便捷地使用 map。接下来,我们就深入探索一下 sync.Map 的工作原理和优势。

sync.Map中的结构体

下面我们先来分析下sync.Map这个包下的一些结构体

1.Map

首先是map这个结构体, 包中最主要的结构体, 也是暴露在外给程序员调用的, 代码片段如下:

type Map struct {
	mu Mutex

	read atomic.Pointer[readOnly]

	dirty map[any]*entry

	misses int
}

接下来我将依次介绍每个变量的含义以及作用

  • mu 是一把互斥锁, 用于保护 dirty map 和 misses 字段的访问。在多 goroutine 情况下,它防止了对这些共享资源的竞争。特别是在向 dirty map 写入数据时,需要持有 mu 锁,以确保写入操作的安全性。
  • read atomic.Pointer 是go提供的一个支持安全并发读写操作的指针类型, 指针指向了一个自定义的结构体readonly (该结构体会在后面讲解), 可以简单理解为 *readOnly
  • dirty 是一个标准的 Go map,用于保存那些已被修改但尚未同步到 read 的数据。dirty 中的数据可以理解为“脏数据”。这个 map 结构通常只有在写操作时才被访问,因此在并发读写操作中,需要持有 mu 锁来确保其操作的安全性。
  • misses 一个记数器, 用于统计从 read 中查找失败(即未命中)次数。当某个 key 在 read 中未找到时,misses 计数器会增加。当 misses 的计数达到某个阈值后,Map 会将 dirty 中的数据同步到 read,然后重置 misses。

2.entry

// 插槽
type entry struct {
	p atomic.Pointer[any]
}

在 sync.Map 中,entry 结构体充当了每个键值对的“插槽”,通过 atomic.Pointer 的原子性来避免加锁,从而提供了高效的并发读写能力。这种设计适合多读少写的场景,可以避免大多数锁操作所带来的性能开销。
p 是一个 atomic.Pointer 类型的指针,指向存储的任意类型的值(any 表示支持任何类型)。

3.readOnly

// 只读变量
type readOnly struct {
	m       map[any]*entry
	amended bool 
}
  • m readOnly 结构体的主要字段,用于存储 sync.Map 中的键值对,供只读访问。m 中的数据是线程安全的,通常是稳定且不需要频繁更新的数据,这样可以实现快速、无锁的读取。
  • amended 是一个布尔值,用来标记 readOnly 是否被修改过。 true代表Map的dirty字段有m中没有的key, 代表dirty已经被修改过, amended 标志来判断是否需要查找 dirty,可以减少不必要的加锁操作。

4.expunged

var expunged = new(any)

在 sync.Map 的实现中,expunged 是一个特殊的标志状态,用于标识一个 entry 已被“移除”或“清除”(通常理解为已删除)。该状态的引入是为了优化内存和性能管理,尤其是在 sync.Map 这种高并发结构中。

syncMap函数逐一解析

经过前面对结构体的介绍, 相信大家已经大概知道map包中每个结构体的作用, 现在我们就开始来解析每一个函数

内置函数(不对外暴露)

loadReadOnly
func (m *Map) loadReadOnly() readOnly {
	if p := m.read.Load(); p != nil {
		return *p
	}
	return readOnly{}
}

该方法获取Map.read中的值, 如果获取为nil,则返回一个readOnly类型的空结构体
可以理解为对指针取值

missLocked
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	m.read.Store(&readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

missLocked 方法用于在读取 sync.Map 时处理未命中的情况。未命中指的是在 read(快速读)中未找到某个 key,需要到 dirty(慢读)中查找。这个方法会记录未命中次数,当次数超过一定阈值后,将 dirty 提升为新的 read,并重置计数器,从而优化未来的读操作性能。

tryExpungeLocked
func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := e.p.Load()
	for p == nil {
		if e.p.CompareAndSwap(nil, expunged) {
			return true
		}
		p = e.p.Load()
	}
	return p == expunged
}

tryExpungeLocked 方法用于尝试将 entry 标记为已删除(expunged),仅在当前值为 nil 时执行。如果当前值不是 nil 或者已经标记为 expunged,则直接返回其状态。

dirtyLocked
func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }

    read := m.loadReadOnly()
    m.dirty = make(map[any]*entry, len(read.m))
    for k, e := range read.m {
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

dirtyLocked 方法用于确保 sync.Map 的 dirty 字段已初始化,并且从 read 中复制未被标记为删除的条目。它在写操作时调用,以便提供一个可写的 dirty 副本。

load
func (e *entry) load() (value any, ok bool) {
	p := e.p.Load()
	if p == nil || p == expunged {
		return nil, false
	}
	return *p, true
}

该方法获取插槽中的值, 如果值等于nil或者该字段已经标记被删除, 则返回空

tryCompareAndSwap
func (e *entry) tryCompareAndSwap(old, new any) bool {
	// 获取entry中的值, 如果p == nil 或者 p == expunged 则表示该entry已经被删除
	// 若 *p != old 则表示当前值与期望的旧值不同,则不执行交换操作
	p := e.p.Load()
	if p == nil || p == expunged || *p != old {
		return false
	}

	// 对nc赋值
	// if e.p.CompareAndSwap(p, &nc):使用 atomic.CompareAndSwap 尝试将 p 更新为 nc,前提是 p 尚未被其他线程修改。
	// 如果交换成功,返回 true,表示更新完成。
	// 如果失败,说明其他线程在此期间修改了 p,需要重新加载当前值。
	nc := new
	for {
		if e.p.CompareAndSwap(p, &nc) {
			return true
		}
		p = e.p.Load()
		if p == nil || p == expunged || *p != old {
			return false
		}
	}
}

该方法用于安全地、无锁地将旧值更新为新值,在满足旧值一致的情况下才执行更新。CAS 操作是一种经典的并发控制手段,尤其适合 sync.Map 这种并发数据结构中的数据更新。

unexpungeLocked
func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return e.p.CompareAndSwap(expunged, nil)
}

unexpungeLocked 是 sync.Map 中用于将一个已标记为删除(expunged)的 entry 恢复为“未使用”状态的函数。其目的是在高并发环境下安全地恢复已清除的条目,通常用于在需要重新写入该键时将其从删除状态切换回来。

swapLocked
func (e *entry) swapLocked(i *any) *any {
	return e.p.Swap(i)
}

swapLocked 方法利用 Swap 操作安全地交换 entry 的值,避免了传统加锁的方式。这使得在并发访问的环境下,可以高效、安全地替换 entry 的值,同时保留了旧值供进一步处理。

tryLoadOrStore
// actual返回实际的值
// loaded表示是否是旧值
// ok表示这次操作是否成功
func (e *entry) tryLoadOrStore(i any) (actual any, loaded, ok bool) {
	p := e.p.Load()
	// 如果entry已经是被标记为删除则不作任何处理
	if p == expunged {
		return nil, false, false
	}
	// 如果能成功加载 则返回加载出来的值
	if p != nil {
		return *p, true, true
	}

	ic := i
	for {
		//  如果原来值为空, 且赋值成功, 返回值
		if e.p.CompareAndSwap(nil, &ic) {
			return i, false, true
		}
		// 如果p 被标记为删除则不作任何处理, 返回false
		p = e.p.Load()
		if p == expunged {
			return nil, false, false
		}
		// 如果p 不为空, 则返回数据
		if p != nil {
			return *p, true, true
		}
	}
}

tryLoadOrStore 方法是 sync.Map 用于在并发环境中安全地加载或存储一个值的函数。它可以在满足一定条件的情况下尝试存储新值,但如果该 entry 已被标记为删除 (expunged),则不执行任何操作。

delete
func (e *entry) delete() (value any, ok bool) {
	for {
		p := e.p.Load()
		if p == nil || p == expunged {
			return nil, false
		}
		if e.p.CompareAndSwap(p, nil) {
			return *p, true
		}
	}
}

delete 方法用于从 sync.Map 中删除一个 entry,并返回该 entry 的值。如果成功删除,它会返回删除的值并指示操作成功;如果该 entry 已经是 nil 或已被标记为删除(expunged),则返回 nil 和 false。

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
		}
	}
}

trySwap 方法用于尝试将 entry 中的值原子性地替换为新值 i,并返回替换之前的值。如果 entry 已经被标记为删除(即 expunged),则不会执行替换操作,直接返回 nil 和 false。

可调用函数(对外暴露)

Load
func (m *Map) Load(key any) (value any, ok bool) {
    // 1. 加载只读映射
    read := m.loadReadOnly()
    e, ok := read.m[key]
    
    // 2. 如果 key 不在只读映射中,且 read.amended 为 true
    if !ok && read.amended {
        m.mu.Lock()
        
        // 重新加载只读映射,确保数据的最新状态
        read = m.loadReadOnly()
        e, ok = read.m[key]
        
        // 3. 若仍然没找到,检查 dirty map 中的数据
        if !ok && read.amended {
            e, ok = m.dirty[key]
            
            // 增加 miss 计数,优化下次读取
            m.missLocked()
        }
        
        m.mu.Unlock()
    }
    
    // 4. 返回值
    if !ok {
        return nil, false
    }
    return e.load()
}

Load 方法是 sync.Map 的读取操作,用于尝试在并发安全的环境下从 sync.Map 中获取指定键的值。

clear
func (m *Map) Clear() {
    // 1. 加载只读映射
    read := m.loadReadOnly()
    
    // 如果 `read` 已为空且未修改,不必执行清空操作,直接返回
    if len(read.m) == 0 && !read.amended {
        return
    }

    // 2. 加锁,确保线程安全
    m.mu.Lock()
    defer m.mu.Unlock()

    // 3. 再次加载只读映射,检查是否仍需要清空
    read = m.loadReadOnly()
    if len(read.m) > 0 || read.amended {
        // 将 `read` 设置为一个新的、空的 `readOnly` 结构
        m.read.Store(&readOnly{})
    }

    // 4. 清空 `dirty` 映射,并重置 `misses` 计数
    clear(m.dirty)
    m.misses = 0
}

Clear 方法用于清空 sync.Map,删除其中的所有键值对。

LoadOrStore
func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool) {
    // 1. 尝试从只读映射中查找键
    read := m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        actual, loaded, ok := e.tryLoadOrStore(value)
        if ok {
            return actual, loaded
        }
    }

    // 2. 加锁处理脏映射和并发情况
    m.mu.Lock()
    read = m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() {
            m.dirty[key] = e
        }
        actual, loaded, _ = e.tryLoadOrStore(value)
    } else if e, ok := m.dirty[key]; ok {
        actual, loaded, _ = e.tryLoadOrStore(value)
        m.missLocked()
    } else {
        if !read.amended {
            m.dirtyLocked()
            m.read.Store(&readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
        actual, loaded = value, false
    }
    m.mu.Unlock()

    return actual, loaded
}

LoadOrStore 方法用于在 sync.Map 中查找一个键值对,如果该键存在,则返回键的值;如果不存在,则存储提供的值并返回。该方法还会返回一个布尔值,指示值是从存储中加载的 (true) 还是新存储的 (false)。

LoadAndDelete
func (m *Map) LoadAndDelete(key any) (value any, loaded bool) {
    // 1. 首先从只读映射中查找键
    read := m.loadReadOnly()
    e, ok := read.m[key]
    if !ok && read.amended {
        // 2. 如果在只读映射中未找到,并且只读映射被标记为已修改(即有未同步的脏数据),加锁处理
        m.mu.Lock()
        read = m.loadReadOnly()
        e, ok = read.m[key]
        if !ok && read.amended {
            // 3. 在脏映射中查找并删除该键
            e, ok = m.dirty[key]
            delete(m.dirty, key)
            // 记录一次“miss”,表示从脏映射中读取
            m.missLocked()
        }
        m.mu.Unlock()
    }

    // 4. 如果键存在,调用 `delete` 方法删除键并返回值
    if ok {
        return e.delete()
    }
    return nil, false
}

LoadAndDelete 方法用于在 sync.Map 中加载并删除指定的键。如果键存在,它会返回键对应的值并将其从映射中删除。返回的布尔值 loaded 指示键是否存在

Delete
func (m *Map) Delete(key any) {
	m.LoadAndDelete(key)
}

Delete方法直接删除, 没有返回值

Swap
func (m *Map) Swap(key, value any) (previous any, loaded bool) {
    // 1. 首先尝试在只读映射中查找该键
    read := m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        if v, ok := e.trySwap(&value); ok {
            if v == nil {
                return nil, false
            }
            return *v, true
        }
    }

    // 2. 如果键未找到或需要更新锁定操作
    m.mu.Lock()
    read = m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() {
            // 键曾被删除,因此存在非空的脏映射,该键不在脏映射中
            m.dirty[key] = e
        }
        if v := e.swapLocked(&value); v != nil {
            loaded = true
            previous = *v
        }
    } else if e, ok := m.dirty[key]; ok {
        // 在脏映射中找到键,交换值
        if v := e.swapLocked(&value); v != nil {
            loaded = true
            previous = *v
        }
    } else {
        // 键在脏映射中不存在,添加新键
        if !read.amended {
            // 首次添加新键到脏映射,确保分配并将只读映射标记为不完整
            m.dirtyLocked()
            m.read.Store(&readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()

    return previous, loaded
}

Swap 方法用于在 sync.Map 中交换指定键的值。如果键存在,返回交换前的旧值;如果键不存在,则将新值存入映射中。loaded 表示键是否已存在。

CompareAndSwap
func (m *Map) CompareAndSwap(key, old, new any) (swapped bool) {
    // 1. 首先在只读映射中查找键
    read := m.loadReadOnly()
    if e, ok := read.m[key]; ok {
        return e.tryCompareAndSwap(old, new)
    } else if !read.amended {
        return false // 如果键不存在且未标记为修订,直接返回 false
    }

    // 2. 如果键不存在或需要更新,进行加锁操作
    m.mu.Lock()
    defer m.mu.Unlock()

    // 重新检查读取状态,防止竞态条件
    read = m.loadReadOnly()
    swapped = false

    if e, ok := read.m[key]; ok {
        swapped = e.tryCompareAndSwap(old, new)
    } else if e, ok := m.dirty[key]; ok {
        swapped = e.tryCompareAndSwap(old, new)
        // 记录一次未命中以确保将脏映射提升为只读映射,提高效率
        m.missLocked()
    }

    return swapped
}

CompareAndSwap 方法在 sync.Map 中用于对特定键的值进行条件性交换操作。如果当前存储的值与 old 相等,则将其替换为 new。此方法确保只有在特定条件满足时(即旧值匹配)才进行替换,符合 “compare-and-swap” 的原子操作模式。

CompareAndDelete
func (m *Map) CompareAndDelete(key, old any) (deleted bool) {
    // 1. 尝试从只读映射中读取键
    read := m.loadReadOnly()
    e, ok := read.m[key]
    if !ok && read.amended {
        // 若键不在只读映射中,但有修订标记,则进入锁定部分
        m.mu.Lock()
        read = m.loadReadOnly()
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // 不从 dirty 中删除键,因为还需要比较。
            // 当 dirty 提升为 read 时,条目将被删除。
            m.missLocked() // 记录未命中,以提升 dirty
        }
        m.mu.Unlock()
    }

    // 2. 对比并删除
    for ok {
        p := e.p.Load()
        if p == nil || p == expunged || *p != old {
            return false // 不匹配,返回 false
        }
        if e.p.CompareAndSwap(p, nil) {
            return true // 匹配并成功删除,返回 true
        }
    }
    return false
}

}
CompareAndDelete 方法在 sync.Map 中用于条件性删除键的值,仅当当前存储的值等于给定的 old 值时才进行删除。这种操作模式确保了对删除的条件性控制,避免误删不匹配的值。

Range
func (m *Map) Range(f func(key, value any) bool) {
    // 1. 加载当前的只读映射
    read := m.loadReadOnly()
    if read.amended {
        // 若有 `dirty` 数据,锁定并升级映射
        m.mu.Lock()
        read = m.loadReadOnly()
        if read.amended {
            // 将 `dirty` 提升为只读映射,避免后续锁定
            read = readOnly{m: m.dirty}
            copyRead := read
            m.read.Store(&copyRead)
            m.dirty = nil
            m.misses = 0
        }
        m.mu.Unlock()
    }

    // 2. 遍历只读映射并执行回调
    for k, e := range read.m {
        v, ok := e.load()
        if !ok {
            continue // 跳过被删除的键
        }
        if !f(k, v) {
            break // 若回调返回 false,则提前退出
        }
    }
}

Range 方法用于遍历 sync.Map 中的所有键值对,并为每个键值对执行一个用户提供的回调函数 f。该方法通过优化读取和并发控制来提高遍历效率,确保 Range 可以高效地处理动态数据。

sync.Map背后的设计细节

1. 结构设计:分离的 read-only 和 dirty 映射

sync.Map 内部将数据分为两部分:一个只读的 read-only 映射和一个可变的 dirty 映射。

  • 只读映射 read-only:
    • 包含了 map 中当前的大部分数据,并且不会发生改变。
    • read-only 是常驻内存的,并且只在读操作时使用。因为 read-only 不需要同步更新,可以直接被并发读操作安全访问,无需锁定。
    • sync.Map 中的 read-only 结构包含一个 map 和一个布尔字段 amended,用于标记是否存在 dirty 数据。
  • 可变映射 dirty:
    • 当执行写操作或更新操作(如插入新键)时,新的或更新的键值对会被写入 dirty 映射。
    • dirty 映射并不总是直接可见给所有读操作,只有当 read-only 被标记为已过期时,数据会被从 dirty 提升至 read-only。

2. 读操作为何如此高效?

由于 sync.Map 将大多数数据放在 read-only 映射中,读取只需要直接访问 read-only,并且由于 read-only 是不变的,因此读操作完全不需要加锁。

  • 只读无锁:在高并发环境下,多个 goroutine 可以并发访问 read-only 映射,无需锁定,极大地提升了读操作的效率。
  • 脏数据判断:sync.Map 使用 amended 标记位来指示是否存在新写入的 dirty 数据,只有当键在 read-only 中找不到时,才会进一步检查 dirty 映射,这在减少读操作耗时的同时,也能避免频繁访问较慢的 dirty。

3. 写操作为何较慢?

写操作在 sync.Map 中较慢,主要是因为以下原因:

  • 锁定开销:由于 dirty 映射的可变性,每次写操作都必须锁定整个 Map,以防止并发修改 dirty 或将 dirty 提升为 read-only 过程中的竞争条件。这种锁定导致写操作的开销较高。
  • dirty 提升和清理:当写操作频繁导致 misses 次数超过阈值时,sync.Map 会将 dirty 的所有数据拷贝到 read-only 映射并清空 dirty,这是一种较重的操作。
  • Range 操作的写入惩罚:Range 操作会强制将 dirty 数据提升至 read-only,并清空 dirty,以便在遍历期间数据保持一致。这样设计避免了写时锁定,但也意味着 Range 之后的第一次写入会导致新的 dirty 分配。

总结

总而言之, sync.Map 是 Go 标准库中的一个并发安全的 map 实现,专为高并发读操作设计。在大多数读取操作中,sync.Map 采用了“只读”和“脏数据”分离的方式,以提高效率。当进行读操作时,如果数据在只读部分已经存在,直接返回而不需要加锁,这使得读取操作非常高效。对于写操作,它会先修改脏数据区域,而不是立即修改只读区域,这样就避免了频繁的加锁。但当脏数据需要同步到只读区域时,它会加锁并进行内存拷贝,导致写操作的性能相对较差。因此,sync.Map 更适合那些读多写少的场景,在这种情况下,它能够通过减少锁的争用,显著提高并发读取的效率,而写操作的开销则相对较高。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ambition!6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值