返回顶部
首页 > 资讯 > 后端开发 > GO >Golang中的sync.Map怎么使用
  • 420
分享到

Golang中的sync.Map怎么使用

2023-07-05 01:07:12 420人浏览 独家记忆
摘要

今天小编给大家分享一下golang中的sync.Map怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。map 在并发下

今天小编给大家分享一下golang中的sync.Map怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

map 在并发下的问题

如果我们看过 map源码,就会发现其中有不少会引起 fatal 错误的地方,比如 mapaccess1(从 map 中读取 key 的函数)里面,如果发现正在写 map,则会有 fatal 错误。 

if h.flags&hashWriting != 0 {    fatal("concurrent map read and map write")}

map 并发读写异常的例子

下面是一个实际使用中的例子:

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)

这会导致报错:

fatal error: concurrent map read and map write

这是因为我们同时对 map 进行读写,而 map 不支持并发读写,所以会报错。如果 map 允许并发读写,那么可能在我们使用的时候会有很多错乱的情况出现。(具体如何错乱,我们可以对比多线程的场景思考一下,本文不展开了)。

使用 sync.Mutex 保证并发安全

对于 map 并发读写报错的问题,其中一种解决方案就是使用 sync.Mutex 来保证并发安全,但是这样会导致我们在读写的时候,都需要加,这样就会导致性能的下降。

使用 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.RWMutex 保证并发安全

在上一小节中,我们使用了 sync.Mutex 来保证并发安全,但是在读和写的时候我们都需要加互斥锁。这就意味着,就算多个协程进行并发读,也需要等待锁。但是互斥锁的粒度太大了,但实际上,并发读是没有什么太大问题的,应该被允许才对,如果我们允许并发读,那么就可以提高性能

当然 go 的开发者也考虑到了这一点,所以在 sync 包中提供了 sync.RWMutex,这个锁可以允许进行并发读,但是写的时候还是需要等待锁。也就是说,一个协程在持有写锁的时候,其他协程是既不能读也不能写的,只能等待写锁释放才能进行读写

使用 sync.RWMutex 来保证并发安全,我们可以改成下面这样:

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)

这样就不会报错了,而且性能也提高了,因为我们在读的时候,不需要等待锁。

说明:

  • 多个协程可以同时使用 RLock 而不需要等待,这是读锁。

  • 只有一个协程可以使用 Lock,这是写锁,有写锁的时候,其他协程不能读也不能写。

  • 持有写锁的协程,可以使用 Unlock 来释放锁。

  • 写锁释放之后,其他协程才能获取到锁(读锁或者写锁)。

也就是说,使用 sync.RWMutex 的时候,读操作是可以并发执行的,但是写操作是互斥的。这样一来,相比 sync.Mutex 来说等待锁的次数就少了,自然也就能获得更好的性能了。

gin 框架里面就使用了 sync.RWMutex 来保证 Keys 读写操作的并发安全。

有了读写锁为什么还要有 sync.Map?

通过上面的内容,我们知道了,有下面两种方式可以保证并发安全:

  • 使用 sync.Mutex,但是这样的话,读写都是互斥的,性能不好。

  • 使用 sync.RWMutex,可以并发读,但是写的时候是互斥的,性能相对 sync.Mutex 要好一些。

但是就算我们使用了 sync.RWMutex,也还是有一些锁的开销。那么我们能不能再优化一下呢?答案是可以的。那就是使用 sync.Map

sync.Map 在锁的基础上做了进一步优化,在一些场景下使用原子操作来保证并发安全,性能更好。

使用原子操作替代读锁

但是就算使用 sync.RWMutex,读操作依然还有锁的开销,那么有没有更好的方式呢?答案是有的,就是使用原子操作来替代读锁。

举一个很常见的例子就是多个协程同时读取一个变量,然后对这个变量进行累加操作:

var a int32var wg sync.WaitGroupwg.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.Int32var wg sync.WaitGroupwg.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/opBenchmarkAtomicAdd-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/opBenchmarkAtomic-12     1000000000          0.3133 ns/op

我们可以看到,使用原子操作的性能比使用锁的性能要好很多。而且在 BenchmarkMutex 里面甚至还没有做读取数据的操作。

sync.Map 里面的原子操作

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 的基本用法。

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 中,keyvalue 都是 interface{} 类型的,也就是说,我们可以使用任意类型的 keyvalue。而不像 map,只能存在一种类型的 keyvalue。从这个角度来看,它的类型类似于 map[any]any

另外一个需要注意的是,Range 方法的参数是一个函数,这个函数如果返回 false,那么遍历就会停止。

sync.Map 的使用场景

sync.Map 源码中,已经告诉了我们 sync.Map 的使用场景:

The Map type is optimized for two common use cases: (1) when the entry for a givenkey 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 disjointsets of keys. In these two cases, use of a Map may significantly reduce lockcontention compared to a Go map paired with a separate Mutex or RWMutex.

翻译过来就是,Map 类型针对两种常见用例进行了优化:

  • 当给定 key 的条目只写入一次但读取多次时,如在只会增长的缓存中。(读多写少)

  • 当多个 goroutine 读取、写入和覆盖不相交的键集的条目。(不同 goroutine 操作不同的 key)

在这两种情况下,与 Go map 与单独的 MutexRWMutex 配对相比,使用 sync.Map 可以显著减少锁竞争(很多时候只需要原子操作就可以)。

以上就是“Golang中的sync.Map怎么使用”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注编程网GO频道。

您可能感兴趣的文档:

--结束END--

本文标题: Golang中的sync.Map怎么使用

本文链接: https://lsjlt.com/news/348855.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Golang中的sync.Map怎么使用
    今天小编给大家分享一下Golang中的sync.Map怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。map 在并发下...
    99+
    2023-07-05
  • 一文浅析Golang中的sync.Map
    本篇文章带大家学习Golang,深入理解下Golang中的sync.Map,希望对大家有所帮助!我们知道,go 里面提供了 map 这种类型让我们可以存储键值对数据,但是如果我们在并发的情况下使用 map 的话,就会发现它是不支持并发地进行...
    99+
    2023-05-14
    Golang 后端 Go
  • 使用golang中的sync.Map函数实现并发安全的映射
    标题:使用golang中的sync.Map函数实现并发安全的映射引言:在并发编程中,多个goroutine同时对同一个数据结构进行读写操作,会造成数据竞争和不一致的问题。为了解决这个问题,Go语言提供了sync包中的Map类型,它是并发安全...
    99+
    2023-11-18
    Golang 并发安全 syncMap
  • golang中的.()怎么使用
    这篇文章主要介绍“golang中的.()怎么使用”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“golang中的.()怎么使用”文章能帮助大家解决问题。什么是.()用法?在golang中,.()被称为...
    99+
    2023-07-05
  • Go怎么使用sync.Map来解决map的并发操作问题
    这篇文章主要介绍“Go怎么使用sync.Map来解决map的并发操作问题”,在日常操作中,相信很多人在Go怎么使用sync.Map来解决map的并发操作问题问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Go怎...
    99+
    2023-06-25
  • Golang中的interface怎么使用
    这篇文章主要讲解了“Golang中的interface怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Golang中的interface怎么使用”吧!万能类型interface在Jav...
    99+
    2023-06-27
  • Golang中的RWMutex怎么使用
    本篇内容主要讲解“Golang中的RWMutex怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Golang中的RWMutex怎么使用”吧!RWMutex 的整体模型正如 RWMutex ...
    99+
    2023-07-05
  • Golang中的WaitGroups怎么使用
    本篇内容介绍了“Golang中的WaitGroups怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!什么是WaitGroupsWait...
    99+
    2023-07-05
  • Golang中的sync.Cond怎么使用
    本文小编为大家详细介绍“Golang中的sync.Cond怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“Golang中的sync.Cond怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。1. 基本...
    99+
    2023-07-05
  • Golang中的Mutex怎么使用
    本篇内容介绍了“Golang中的Mutex怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Mutex基本概念Mutex 是 Go 语言...
    99+
    2023-07-05
  • golang中的注释怎么使用
    本篇内容主要讲解“golang中的注释怎么使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“golang中的注释怎么使用”吧!一、单行注释单行注释是在代码行的末尾添加注释的方式,以“//”开头。...
    99+
    2023-07-05
  • golang中怎么使用gopath
    小编给大家分享一下golang中怎么使用gopath,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!golang gopath的新用法从使用golang开发项目以来...
    99+
    2023-06-14
  • golang中channel怎么使用
    今天小编给大家分享一下golang中channel怎么使用的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。什么是 channe...
    99+
    2023-06-29
  • golang中defer怎么使用
    这篇文章主要介绍“golang中defer怎么使用”,在日常操作中,相信很多人在golang中defer怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”golang中defer怎么使用”的疑惑有所帮助!...
    99+
    2023-07-02
  • 怎么使用golang开发中的godep
    本篇内容主要讲解“怎么使用golang开发中的godep”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“怎么使用golang开发中的godep”吧!介绍godep是解决包依赖的管理工具,目前最主流...
    99+
    2023-06-25
  • Golang中的init ()函数怎么使用
    本文小编为大家详细介绍“Golang中的init ()函数怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“Golang中的init ()函数怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。Go in...
    99+
    2023-07-04
  • Golang语言中的map怎么使用
    这篇文章主要介绍“Golang语言中的map怎么使用”,在日常操作中,相信很多人在Golang语言中的map怎么使用问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Golang语言中的map怎么使用”的疑惑有所...
    99+
    2023-07-05
  • bytes.Buffer怎么在golang中使用
    bytes.Buffer怎么在golang中使用?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。go适合做什么go是golang的简称,而golang可以做服务器...
    99+
    2023-06-14
  • Ring怎么在Golang中使用
    今天就跟大家聊聊有关Ring怎么在Golang中使用,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。golang的优点golang是一种编译语言,可以将代码编译为机器代码,编译后的二进...
    99+
    2023-06-14
  • Golang的select怎么使用
    本文小编为大家详细介绍“Golang的select怎么使用”,内容详细,步骤清晰,细节处理妥当,希望这篇“Golang的select怎么使用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。背景golang...
    99+
    2023-07-04
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作