이 글이 Golang을 배우고, Golang의 sync.Map에 대한 깊은 이해를 가지는데 도움이 되길 바랍니다!
go에서 map
这种类型让我们可以存储键值对数据,但是如果我们在并发的情况下使用 map
가 제공되면 동시 읽기 및 쓰기를 지원하지 않는다는 것을 알 수 있습니다(오류가 보고됩니다).
이 경우 sync.Mutex
를 사용하여 동시성 안전성을 보장할 수 있지만 이로 인해 읽고 쓸 때 잠금이 필요하게 되어 성능 저하가 발생합니다.
상대적으로 비효율적인 뮤텍스 잠금 사용 방법 외에도 sync.Map
을 사용하여 동시성 안전성을 보장할 수 있는데, 이는 일부 시나리오에서 sync.Map
来保证并发安全,它在某些场景下有比使用 sync.Mutex
를 사용하는 것보다 성능이 더 높습니다.
이 글에서는 모두가 관심을 갖고 있는 sync.Map
의 몇 가지 문제에 대해 논의할 것입니다. 예를 들어, mapsync.Map
이 필요한가요? /코드>? 왜 빠른가요? sync.Map
에 적용 가능한 시나리오(참고: 모든 경우에 빠르지는 않음) 등 sync.Map
中的一些大家比较感兴趣的问题,比如为什么有了 map
还要 sync.Map
?它为什么快?sync.Map
的适用场景(注意:不是所有情况下都快。)等。
关于 sync.Map
的设计与实现原理,会在下一篇中再做讲解。
如果我们看过 map
的源码,就会发现其中有不少会引起 fatal
错误的地方,比如 mapaccess1
(从 map
中读取 key
的函数)里面,如果发现正在写 map
,则会有 fatal
sync.Map
의 설계 및 구현 원리는 다음 글에서 설명하겠습니다.
map
의 소스 코드를 살펴보면 그 중 많은 문제가 fatal mapaccess1
(map
에서 key
를 읽는 함수)와 같이 잘못된 위치에 있는 경우 지도
하면 치명적인
오류가 발생합니다.
[관련 권장사항: Go 동영상 튜토리얼, if h.flags&hashWriting != 0 { fatal("concurrent map read and map write") }
var m = make(map[int]int) // 往 map 写 key 的协程 go func() { // 往 map 写入数据 for i := 0; i < 10000; i++ { m[i] = i } }() // 从 map 读取 key 的协程 go func() { // 从 map 读取数据 for i := 10000; i > 0; i-- { _ = m[i] } }() // 等待两个协程执行完毕 time.Sleep(time.Second)
map
进行读写,而 map
不支持并发读写,所以会报错。如果 map
이렇게 하면 오류가 발생합니다.fatal error: concurrent map read and map write
sync.Mutex를 사용하여 동시성 안전성 보장map
并发读写报错的问题,其中一种解决方案就是使用 sync.Mutex
map
동시 읽기 및 쓰기 오류 문제에 대한 한 가지 해결책은 을 사용하여 동시성 안전성을 보장하는 것입니다.
그러나 이로 인해 읽고 쓸 때 잠금이 필요하게 되어 성능이 저하됩니다. sync.Mutex
来保证并发安全,上面的代码可以改成下面这样:
var m = make(map[int]int) // 互斥锁 var mu sync.Mutex // 写 map 的协程 go func() { for i := 0; i < 10000; i++ { mu.Lock() // 写 map,加互斥锁 m[i] = i mu.Unlock() } }() // 读 map 的协程序 go func() { for i := 10000; i > 0; i-- { mu.Lock() // 读 map,加互斥锁 _ = m[i] mu.Unlock() } }() time.Sleep(time.Second)
这样就不会报错了,但是性能会有所下降,因为我们在读写的时候都需要加锁。(如果需要更高性能,可以继续读下去,不要急着使用 sync.Mutex
)
sync.Mutex
的常见的用法是在结构体中嵌入sync.Mutex
,而不是定义独立的两个变量。
在上一小节中,我们使用了 sync.Mutex
동시성 안전을 보장하려면 를 사용하세요. 위 코드를 다음과 같이 변경할 수 있습니다. var m = make(map[int]int)
// 读写锁(允许并发读,写的时候是互斥的)
var mu sync.RWMutex
// 写入 map 的协程
go func() {
for i := 0; i < 10000; i++ {
// 写入的时候需要加锁
mu.Lock()
m[i] = i
mu.Unlock()
}
}()
// 读取 map 的协程
go func() {
for i := 10000; i > 0; i-- {
// 读取的时候需要加锁,但是这个锁是读锁
// 多个协程可以同时使用 RLock 而不需要等待
mu.RLock()
_ = m[i]
mu.RUnlock()
}
}()
// 另外一个读取 map 的协程
go func() {
for i := 20000; i > 10000; i-- {
// 读取的时候需要加锁,但是这个锁是读锁
// 多个协程可以同时使用 RLock 而不需要等待
mu.RLock()
_ = m[i]
mu.RUnlock()
}
}()
time.Sleep(time.Second)
sync
包中提供了sync.RWMutex
의 일반적인 사용법은 두 개의 독립 변수를 정의하는 대신 구조에 를 삽입하는 것입니다.
sync.RWMutex
来保证并发安全,我们可以改成下面这样:
var a int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() wg.Wait() // a 期望结果应该是 20000才对。 fmt.Println(a) // 实际:17089,而且每次都不一样
这样就不会报错了,而且性能也提高了,因为我们在读的时候,不需要等待锁。
说明:
RLock
而不需要等待,这是读锁。Lock
,这是写锁,有写锁的时候,其他协程不能读也不能写。Unlock
来释放锁。也就是说,使用 sync.RWMutex
이전 섹션에서는 동시성 안전성을 보장하기 위해 sync.Mutex
를 사용했지만 읽고 쓸 때 모두 필요합니다. 뮤텍스 잠금.
즉,
sync.RWMutex
를 사용하여 다음과 같이 변경할 수 있습니다. 🎜var a atomic.Int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() wg.Wait() fmt.Println(a.Load()) // 20000
RLock
을 사용할 수 있습니다. 이는 읽기 잠금입니다. Lock
을 사용할 수 있습니다. 쓰기 잠금이 있으면 다른 코루틴은 읽거나 쓸 수 없습니다. Unlock
을 사용하여 잠금을 해제할 수 있습니다. sync.RWMutex
를 사용하면 읽기 작업은 동시에 실행할 수 있지만 쓰기 작업은 상호 배타적입니다.
이렇게 하면 🎜에 비해 잠금을 기다리는 횟수가 줄어들고 자연스럽게 더 나은 성능을 얻을 수 있습니다. 🎜gin 框架里面就使用了
sync.RWMutex
来保证Keys
读写操作的并发安全。
通过上面的内容,我们知道了,有下面两种方式可以保证并发安全:
sync.Mutex
,但是这样的话,读写都是互斥的,性能不好。sync.RWMutex
,可以并发读,但是写的时候是互斥的,性能相对 sync.Mutex
要好一些。但是就算我们使用了 sync.RWMutex
,也还是有一些锁的开销。那么我们能不能再优化一下呢?答案是可以的。那就是使用 sync.Map
。
sync.Map
在锁的基础上做了进一步优化,在一些场景下使用原子操作来保证并发安全,性能更好。
但是就算使用 sync.RWMutex
,读操作依然还有锁的开销,那么有没有更好的方式呢?
答案是有的,就是使用原子操作来替代读锁。
举一个很常见的例子就是多个协程同时读取一个变量,然后对这个变量进行累加操作:
var a int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a++ } wg.Done() }() wg.Wait() // a 期望结果应该是 20000才对。 fmt.Println(a) // 实际:17089,而且每次都不一样
这个例子中,我们期望的结果是 a
的值是 20000
,但是实际上,每次运行的结果都不一样,而且都不会等于 20000
。
其中很简单粗暴的一种解决方法是加锁,但是这样的话,性能就不好了,但是我们可以使用原子操作来解决这个问题:
var a atomic.Int32 var wg sync.WaitGroup wg.Add(2) go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() go func() { for i := 0; i < 10000; i++ { a.Add(1) } wg.Done() }() wg.Wait() fmt.Println(a.Load()) // 20000
我们来看一下,使用锁和原子操作的性能差多少:
func BenchmarkMutexAdd(b *testing.B) { var a int32 var mu sync.Mutex for i := 0; i < b.N; i++ { mu.Lock() a++ mu.Unlock() } } func BenchmarkAtomicAdd(b *testing.B) { var a atomic.Int32 for i := 0; i < b.N; i++ { a.Add(1) } }
结果:
BenchmarkMutexAdd-12 100000000 10.07 ns/op BenchmarkAtomicAdd-12 205196968 5.847 ns/op
我们可以看到,使用原子操作的性能比使用锁的性能要好一些。
也许我们会觉得上面这个例子是写操作,那么读操作呢?我们来看一下:
func BenchmarkMutex(b *testing.B) { var mu sync.RWMutex for i := 0; i < b.N; i++ { mu.RLock() mu.RUnlock() } } func BenchmarkAtomic(b *testing.B) { var a atomic.Int32 for i := 0; i < b.N; i++ { _ = a.Load() } }
结果:
BenchmarkMutex-12 100000000 10.12 ns/op BenchmarkAtomic-12 1000000000 0.3133 ns/op
我们可以看到,使用原子操作的性能比使用锁的性能要好很多。而且在 BenchmarkMutex
里面甚至还没有做读取数据的操作。
sync.Map
里面相比 sync.RWMutex
,性能更好的原因就是使用了原子操作。
在我们从 sync.Map
里面读取数据的时候,会先使用一个原子 Load
操作来读取 sync.Map
里面的 key
(从 read
中读取)。
注意:这里拿到的是 key
的一份快照,我们对其进行读操作的时候也可以同时往 sync.Map
中写入新的 key
,这是保证它高性能的一个很关键的设计(类似读写分离)。
sync.Map
里面的 Load
方法里面就包含了上述的流程:
// Load 方法从 sync.Map 里面读取数据。 func (m *Map) Load(key any) (value any, ok bool) { // 先从只读 map 里面读取数据。 // 这一步是不需要锁的,只有一个原子操作。 read := m.loadReadOnly() e, ok := read.m[key] if !ok && read.amended { // 如果没有找到,并且 dirty 里面有一些 read 中没有的 key,那么就需要从 dirty 里面读取数据。 // 这里才需要锁 m.mu.Lock() read = m.loadReadOnly() e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] m.missLocked() } m.mu.Unlock() } // key 不存在 if !ok { return nil, false } // 使用原子操作读取 return e.Load() }
上面的代码我们可能还看不懂,但是没关系,这里我们只需要知道的是,从 sync.Map 读取数据的时候,会先做原子操作,如果没找到,再进行加锁操作,这样就减少了使用锁的频率了,自然也就可以获得更好的性能(但要注意的是并不是所有情况下都能获得更好的性能)。至于具体实现,在下一篇文章中会进行更加详细的分析。
也就是说,sync.Map 之所以更快,是因为相比 RWMutex,进一步减少了锁的使用,而这也就是 sync.Map 存在的原因了
现在我们知道了,sync.Map
里面是利用了原子操作来减少锁的使用。但是我们好像连 sync.Map
的一些基本操作都还不了解,现在就让我们再来看看 sync.Map
的基本用法。
sync.Map
的使用还是挺简单的,map
中有的操作,在 sync.Map
都有,只不过区别是,在 sync.Map
中,所有的操作都需要通过调用其方法来进行。sync.Map
里面几个常用的方法有(CRUD
):
Store
:我们新增或者修改数据的时候,都可以使用 Store
方法。Load
:读取数据的方法。Range
:遍历数据的方法。Delete
:删除数据的方法。var m sync.Map // 写入/修改 m.Store("foo", 1) // 读取 fmt.Println(m.Load("foo")) // 1 true // 遍历 m.Range(func(key, value interface{}) bool { fmt.Println(key, value) // foo 1 return true }) // 删除 m.Delete("foo") fmt.Println(m.Load("foo")) // nil false
注意:在 sync.Map
中,key
和 value
都是 interface{}
类型的,也就是说,我们可以使用任意类型的 key
和 value
。
而不像 map
,只能存在一种类型的 key
和 value
。从这个角度来看,它的类型类似于 map[any]any
。
另外一个需要注意的是,Range
方法的参数是一个函数,这个函数如果返回 false
,那么遍历就会停止。
在 sync.Map
源码中,已经告诉了我们 sync.Map
的使用场景:
The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.
翻译过来就是,Map 类型针对两种常见用例进行了优化:
key
的条目只写入一次但读取多次时,如在只会增长的缓存中。(读多写少)在这两种情况下,与 Go map
与单独的 Mutex
或 RWMutex
配对相比,使用 sync.Map
可以显著减少锁竞争(很多时候只需要原子操作就可以)。
map
不支持并发读写。map
的并发读写:sync.Mutex
互斥锁。读和写的时候都使用互斥锁,性能相比 sync.RWMutex
会差一些。sync.RWMutex
读写锁。读的锁是可以共享的,但是写锁是独占的。性能相比 sync.Mutex
会好一些。sync.Map
里面会先进行原子操作来读取 key
,如果读取不到的时候,才会需要加锁。所以性能相比 sync.Mutex
和 sync.RWMutex
会好一些。sync.Map
里面几个常用的方法有(CRUD
):Store
:我们新增或者修改数据的时候,都可以使用 Store
方法。Load
:读取数据的方法。Range
:遍历数据的方法。Delete
:删除数据的方法。sync.Map
的使用场景,sync.Map
针对以下两种场景做了优化:key
只会写入一次,但是会被读取多次的场景。更多编程相关知识,请访问:编程视频!!
위 내용은 Golang의 sync.Map에 대한 간략한 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!