首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | Mutex:如何解决资源并发访问问题?
02 | Mutex:庖丁解牛看实现
03|Mutex:4种易错场景大盘点
04| Mutex:骇客编程,如何拓展额外功能?
05| RWMutex:读写锁的实现原理及避坑指南
06 | WaitGroup:协同等待,任务编排利器
07 | Cond:条件变量的实现机制及避坑指南
08 | Once:一个简约而不简单的并发原语
09 | map:如何实现线程安全的map类型?
10 | Pool:性能提升大杀器
11 | Context:信息穿透上下文
12 | atomic:要保证原子操作,一定要使用这几种方法
13 | Channel:另辟蹊径,解决并发问题
14 | Channel:透过代码看典型的应用模式
15 | 内存模型:Go如何保证并发读写的顺序?
16 | Semaphore:一篇文章搞懂信号量
17 | SingleFlight 和 CyclicBarrier:请求合并和循环栅栏该怎么用?
18 | 分组操作:处理一组子任务,该用什么并发原语?
19 | 在分布式环境中,Leader选举、互斥锁和读写锁该如何实现?
20 | 在分布式环境中,队列、栅栏和STM该如何实现?
当前位置:
首页>>
技术小册>>
Golang并发编程实战
小册名称:Golang并发编程实战
### 09 | map:如何实现线程安全的map类型? 在Go语言(Golang)中,map是一种内置的数据结构,用于存储键值对集合,它以其高效的查找、插入和删除操作而著称。然而,标准的Go map类型并不是线程安全的,即它们并不是为并发环境设计的。当多个goroutine尝试同时读写同一个map时,可能会导致运行时panic,因为Go的map访问并不包含任何内置的锁机制来同步对map的访问。因此,在并发编程中,实现一个线程安全的map变得尤为重要。 #### 一、线程安全问题的本质 线程安全问题主要源于多个执行线程(在Go中称为goroutine)对共享资源的竞争访问。具体到map,当两个或更多的goroutine同时尝试修改同一个map(如添加或删除键值对)时,可能会因为内部状态的不一致而导致不可预测的行为,甚至引发panic。 #### 二、Go标准库中的解决方案:sync.Map 从Go 1.9版本开始,标准库`sync`中引入了一个新的类型`sync.Map`,它专为并发环境设计,提供了比使用互斥锁(mutex)保护的传统map更高效、更简洁的并发访问方式。`sync.Map`通过减少锁的使用和细粒度的锁策略,优化了高并发场景下的性能。 ##### 2.1 sync.Map的基本使用 `sync.Map`提供了几个关键的方法来实现并发安全的键值对操作: - `Store(key, value interface{})`:存储键值对。 - `Load(key interface{}) (value interface{}, ok bool)`:根据键加载值,如果键存在则返回对应的值和true,否则返回nil和false。 - `LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)`:尝试加载键对应的值,如果键不存在则存储该键值对,并返回键的旧值(如果存在)和一个布尔值表示是否加载了旧值。 - `Delete(key interface{})`:删除键值对。 - `Range(f func(key, value interface{}) bool)`:遍历map中的所有键值对,对每个键值对调用函数f,如果f返回false,则停止遍历。 ##### 2.2 sync.Map的内部机制 `sync.Map`之所以能够在高并发环境下保持高效,主要得益于其内部的复杂设计,包括两个主要部分: - **读map(read map)**:一个不需要加锁即可安全访问的map,用于快速读取操作。当键存在于读map中时,可以直接返回其值,无需任何锁操作。 - **脏map(dirty map)**:一个包含所有键值对的map,包括读map中未包含的条目。脏map的修改需要加锁保护,以保证并发安全。当需要更新或删除键值对时,操作会首先在脏map上进行,然后可能更新读map以优化后续的读取操作。 此外,`sync.Map`还使用了一个miss计数器来跟踪从读map中未找到键的次数。当这个计数器达到一定阈值时,会触发一个称为“提升”的过程,即将脏map的内容全部(或部分)迁移到读map中,并重置miss计数器。这个过程同样需要加锁,但相比直接对读map加锁进行读写操作,其频率要低得多,因此能够显著提高性能。 #### 三、自定义线程安全map的实现 虽然`sync.Map`为并发场景下的map操作提供了高效的解决方案,但在某些特定情况下,开发者可能需要根据自己的需求自定义线程安全的map实现。这通常涉及到使用互斥锁(如`sync.Mutex`或`sync.RWMutex`)来保护对map的访问。 ##### 3.1 使用sync.Mutex ```go type SafeMap struct { m map[interface{}]interface{} mu sync.Mutex } func NewSafeMap() *SafeMap { return &SafeMap{ m: make(map[interface{}]interface{}), } } func (sm *SafeMap) Set(key, value interface{}) { sm.mu.Lock() defer sm.mu.Unlock() sm.m[key] = value } func (sm *SafeMap) Get(key interface{}) (interface{}, bool) { sm.mu.Lock() defer sm.mu.Unlock() value, ok := sm.m[key] return value, ok } func (sm *SafeMap) Delete(key interface{}) { sm.mu.Lock() defer sm.mu.Unlock() delete(sm.m, key) } ``` 上述代码展示了如何使用`sync.Mutex`来创建一个简单的线程安全map。这种方法简单直接,但在高并发场景下可能会因为锁的争用而导致性能瓶颈。 ##### 3.2 使用sync.RWMutex优化读性能 为了优化读性能,可以使用`sync.RWMutex`代替`sync.Mutex`。`sync.RWMutex`允许多个goroutine同时读取map,但在写入时会阻塞所有读取和写入的goroutine。 ```go type SafeMap struct { m map[interface{}]interface{} mu sync.RWMutex } // Set, Get, Delete 方法实现与上述类似,但使用mu.Lock()替换为mu.Lock()在写操作,mu.RLock()和mu.RUnlock()在读操作中 ``` #### 四、性能考量与选择 在选择使用`sync.Map`还是自定义线程安全map时,需要根据实际场景进行权衡。`sync.Map`在处理大量动态变化的键值对时表现尤为出色,因为它通过减少锁的使用和内部优化来提高性能。然而,如果map的更新操作相对较少,且对性能有极致要求,使用互斥锁保护的自定义map可能更为合适。 此外,还需要考虑内存使用和GC(垃圾回收)的影响。`sync.Map`由于其内部机制,可能会比简单的map消耗更多的内存,并且由于它包含多个内部结构和锁,可能会增加GC的压力。 #### 五、总结 在Go并发编程中,实现线程安全的map类型是一项重要且常见的任务。Go标准库提供的`sync.Map`为此提供了高效且易用的解决方案,但在某些特定情况下,开发者也可能需要根据自己的需求自定义线程安全的map实现。无论选择哪种方式,都需要对并发控制、性能优化和内存管理有深入的理解,以确保程序在高并发环境下的稳定性和性能。
上一篇:
08 | Once:一个简约而不简单的并发原语
下一篇:
10 | Pool:性能提升大杀器
该分类下的相关小册推荐:
GO面试指南
深入浅出Go语言核心编程(八)
深入浅出Go语言核心编程(二)
深入浅出Go语言核心编程(一)
深入浅出Go语言核心编程(七)
企业级Go应用开发从零开始
WebRTC音视频开发实战
Go Web编程(上)
Go语言从入门到实战
Go开发基础入门
深入解析go语言
Go 组件设计与实现