在 Go 中,使用竞争集合的工作已经成为一个重要主题,原因是对多线程应用程序的要求日益增加。常规的 map 在 Go 中不是线程安全的,可能会导致数据竞争(data race)。sync.Map 的出现提供了一个标准解决方案,可以安全地共享访问集合而无需外部同步。
问题的历史:
在 sync.Map 出现之前,开发人员必须使用常规的 map 和外部的 Mutex 或 RWMutex 来组织从多个 goroutine 的安全访问。这增加了代码的复杂性和同步错误的概率。在 Go 1.9 中推出了 sync.Map,目的是简化与竞争集合的工作。
问题:
常规的 map 不是线程安全的。如果多个 goroutine 在没有同步的情况下读取和写入 map,这会导致 panic 或意外结果。Mutex 的正确使用较为复杂,可能导致锁的争用和性能下降。同时,"double check" 和处理复杂的同步也带来了困扰。
解决方案:
sync.Map 是标准库中的特殊结构,提供线程安全的方法 Load、Store、LoadOrStore、Delete、Range。它实现了一种部分无锁策略,针对读取频繁和写入稀少的场景进行了优化。
代码示例:
import ( "fmt" "sync" ) func main() { var m sync.Map m.Store("foo", 42) value, ok := m.Load("foo") fmt.Println(value, ok) // 42 true m.Delete("foo") }
主要特点:
是否可以在多线程程序中将所有 map 替换为 sync.Map?
不可以,sync.Map 并不是常规 map 的通用替代品。它非常适合于竞争读取占优的那些数据结构,但在频繁写入(频繁修改)或对于小集合的场景中,常规 map + Mutex 更快且更有效。
如果常规 map 只用于几个 goroutine 进行读取,会发生什么?
如果 map 在所有 goroutine 启动后完全初始化且不再修改,则并行读取是允许且安全的。但任何数据的删除或修改都会导致不可预测的行为、panic 或 corrupted map。
可以使用哪些数据类型作为 sync.Map 的键?
规则与常规 map 相同:仅限于可比较类型(comparable types)。然而,sync.Map 接受任何类型的接口{} 作为键,这可能会带来风险,可能存在语义不同的对象之间无法进行比较或允许运行时错误。
代码示例:
var m sync.Map m.Store([]int{1,2}, "value") // panic: runtime error: hash of unhashable type []int
开发人员使用 sync.Map 存储应用程序的设置,这些设置很少更改,但经常被读取。然而,后来开始大量写入用户会话的数据,这导致 GC 的负担意外增加,性能下降。
优点:
缺点:
团队在高负载服务中实施 sync.Map 来存储经常请求的计算结果缓存。读取操作的数量比写入多数倍。一切工作稳定且高效,代码变得更简洁,更易于维护。
优点:
缺点: