深入探讨Go语言中的map是否是并发安全以及解决方法

 更新时间:2023年05月29日 08:29:12   作者:yongxinz  
这篇文章主要来和大家探讨 Go 语言中的 map 是否是并发安全的,并提供三种方案来解决并发问题,文中的示例代码讲解详细,需要的可以参考一下

Go 语言中的 map 是一个非常常用的数据结构,它允许我们快速地存储和检索键值对。然而,在并发场景下使用 map 时,还是有一些问题需要注意的。

本文将探讨 Go 语言中的 map 是否是并发安全的,并提供三种方案来解决并发问题。

先来回答一下题目的问题,答案就是并发不安全

看一段代码示例,当两个 goroutine 同时对同一个 map 进行写操作时,会发生什么?

package main

import "sync"

func main() {
    m := make(map[string]int)
    m["foo"] = 1

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        for i := 0; i < 1000; i++ {
            m["foo"]++
        }
        wg.Done()
    }()

    go func() {
        for i := 0; i < 1000; i++ {
            m["foo"]++
        }
        wg.Done()
    }()

    wg.Wait()
}

在这个例子中,我们可以看到,两个 goroutine 将尝试同时对 map 进行写入。运行这个程序时,我们将看到一个错误:

fatal error: concurrent map writes

也就是说,在并发场景下,这样操作 map 是不行的。

为什么是不安全的

因为它没有内置的锁机制来保护多个 goroutine 同时对其进行读写操作。

当多个 goroutine 同时对同一个 map 进行读写操作时,就会出现数据竞争和不一致的结果。

就像上例那样,当两个 goroutine 同时尝试更新同一个键值对时,最终的结果可能取决于哪个 goroutine 先完成了更新操作。这种不确定性可能会导致程序出现错误或崩溃。

Go 语言团队没有将 map 设计成并发安全的,是因为这样会增加程序的开销并降低性能。

如果 map 内置了锁机制,那么每次访问 map 时都需要进行加锁和解锁操作,这会增加程序的运行时间并降低性能。

此外,并不是所有的程序都需要在并发场景下使用 map,因此将锁机制内置到 map 中会对那些不需要并发安全的程序造成不必要的开销。

在实际使用过程中,开发人员可以根据程序的需求来选择是否需要保证 map 的并发安全性,从而在性能和安全性之间做出权衡。

如何并发安全

接下来介绍三种并发安全的方式:

  • 读写锁
  • 分片加锁
  • sync.Map

加读写锁

第一种方法是使用读写锁,这是最容易想到的一种方式。在读操作时加读锁,在写操作时加写锁。

package main

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    sync.RWMutex
    Map map[string]string
}

func NewSafeMap() *SafeMap {
    sm := new(SafeMap)
    sm.Map = make(map[string]string)
    return sm
}

func (sm *SafeMap) ReadMap(key string) string {
    sm.RLock()
    value := sm.Map[key]
    sm.RUnlock()
    return value
}

func (sm *SafeMap) WriteMap(key string, value string) {
    sm.Lock()
    sm.Map[key] = value
    sm.Unlock()
}

func main() {
    safeMap := NewSafeMap()

    var wg sync.WaitGroup

    // 启动多个goroutine进行写操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
        }(i)
    }

    wg.Wait()

    // 启动多个goroutine进行读操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i)))
        }(i)
    }

    wg.Wait()
}

在这个示例中,我们定义了一个 SafeMap 结构体,它包含一个 sync.RWMutex 和一个 map[string]string

定义了两个方法:ReadMap 和 WriteMap。在 ReadMap 方法中,我们使用读锁来保护对 map 的读取操作。在 WriteMap 方法中,我们使用写锁来保护对 map 的写入操作。

在 main 函数中,我们启动了多个 goroutine 来进行读写操作,这些操作都是安全的。

分片加锁

上例中通过对整个 map 加锁来实现需求,但相对来说,锁会大大降低程序的性能,那如何优化呢?其中一个优化思路就是降低锁的粒度,不对整个 map 进行加锁。

这种方法是分片加锁,将这个 map 分成 n 块,每个块之间的读写操作都互不干扰,从而降低冲突的可能性。

package main

import (
    "fmt"
    "sync"
)

const N = 16

type SafeMap struct {
    maps  [N]map[string]string
    locks [N]sync.RWMutex
}

func NewSafeMap() *SafeMap {
    sm := new(SafeMap)
    for i := 0; i < N; i++ {
        sm.maps[i] = make(map[string]string)
    }
    return sm
}

func (sm *SafeMap) ReadMap(key string) string {
    index := hash(key) % N
    sm.locks[index].RLock()
    value := sm.maps[index][key]
    sm.locks[index].RUnlock()
    return value
}

func (sm *SafeMap) WriteMap(key string, value string) {
    index := hash(key) % N
    sm.locks[index].Lock()
    sm.maps[index][key] = value
    sm.locks[index].Unlock()
}

func hash(s string) int {
    h := 0
    for i := 0; i < len(s); i++ {
        h = 31*h + int(s[i])
    }
    return h
}

func main() {
    safeMap := NewSafeMap()

    var wg sync.WaitGroup

    // 启动多个goroutine进行写操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            safeMap.WriteMap(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
        }(i)
    }

    wg.Wait()

    // 启动多个goroutine进行读操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            fmt.Println(safeMap.ReadMap(fmt.Sprintf("name%d", i)))
        }(i)
    }

    wg.Wait()
}

在这个示例中,我们定义了一个 SafeMap 结构体,它包含一个长度为 N 的 map 数组和一个长度为 N 的锁数组。

定义了两个方法:ReadMap 和 WriteMap。在这两个方法中,我们都使用了一个 hash 函数来计算 key 应该存储在哪个 map 中。然后再对这个 map 进行读写操作。

在 main 函数中,我们启动了多个 goroutine 来进行读写操作,这些操作都是安全的。

有一个开源项目 orcaman/concurrent-map 就是通过这种思想来做的,感兴趣的同学可以看看。

sync.Map

最后,在内置的 sync 包中(Go 1.9+)也有一个线程安全的 map,通过将读写分离的方式实现了某些特定场景下的性能提升。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    var wg sync.WaitGroup

    // 启动多个goroutine进行写操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            m.Store(fmt.Sprintf("name%d", i), fmt.Sprintf("John%d", i))
        }(i)
    }

    wg.Wait()

    // 启动多个goroutine进行读操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            v, _ := m.Load(fmt.Sprintf("name%d", i))
            fmt.Println(v.(string))
        }(i)
    }

    wg.Wait()
}

有了官方的支持,代码瞬间少了很多,使用起来方便多了。

在这个示例中,我们使用了内置的 sync.Map 类型来存储键值对,使用 Store 方法来存储键值对,使用 Load 方法来获取键值对。

在 main 函数中,我们启动了多个 goroutine 来进行读写操作,这些操作都是安全的。

总结

Go 语言中的 map 本身并不是并发安全的。

在多个 goroutine 同时访问同一个 map 时,可能会出现并发不安全的现象。这是因为 Go 语言中的 map 并没有内置锁来保护对map的访问。

尽管如此,我们仍然可以使用一些方法来实现 map 的并发安全。

一种方法是使用读写锁,在读操作时加读锁,在写操作时加写锁。

另一种方法是分片加锁,将这个 map 分成 n 块,每个块之间的读写操作都互不干扰,从而降低冲突的可能性。

此外,在内置的 sync 包中(Go 1.9+)也有一个线程安全的 map,它通过将读写分离的方式实现了某些特定场景下的性能提升。

到此这篇关于深入探讨Go语言中的map是否是并发安全以及解决方法的文章就介绍到这了,更多相关Go语言map内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • go内存缓存BigCache使用入门详解

    go内存缓存BigCache使用入门详解

    这篇文章主要为大家介绍了go内存缓存BigCache使用入门详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • golang使用 gomodule 在公共测试环境管理go的依赖的实例详解

    golang使用 gomodule 在公共测试环境管理go的依赖的实例详解

    这篇文章主要介绍了golang使用 gomodule 在公共测试环境管理go的依赖,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • go语言使用Casbin实现角色的权限控制

    go语言使用Casbin实现角色的权限控制

    Casbin是用于Golang项目的功能强大且高效的开源访问控制库。本文主要介绍了go语言使用Casbin实现角色的权限控制,感兴趣的可以了解下
    2021-06-06
  • Go处理包含多种引号的字符串的几种方法

    Go处理包含多种引号的字符串的几种方法

    在Go中,有几种方式可以处理包含多种引号的字符串,以确保代码的可读性和正确性,本文将给大家详细介绍了这几种处理方式,并通过代码示例讲解的非常详细,需要的朋友可以参考下
    2024-04-04
  • golang中bufio.SplitFunc的深入理解

    golang中bufio.SplitFunc的深入理解

    这篇文章主要给大家介绍了关于golang中bufio.SplitFunc的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用golang具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-10-10
  • 在goland中配置gofmt的操作

    在goland中配置gofmt的操作

    这篇文章主要介绍了在goland中配置gofmt的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • GO中的时间操作总结(time&dateparse)

    GO中的时间操作总结(time&dateparse)

    日常开发过程中,对于时间的操作可谓是无处不在,但是想实现时间自由还是不简单的,多种时间格式容易混淆,本文为大家整理了一下GO中的时间操作,有需要的可以参考下
    2023-09-09
  • go打印%v %+v %#v的区别及说明

    go打印%v %+v %#v的区别及说明

    文章总结了Go语言中fmt包中%v、%+v和%#v格式化动词的用法:%v只输出值,%+v输出字段名和值,%#v输出结构体名和字段信息
    2025-12-12
  • 基于Go语言实现类似tree命令的小程序

    基于Go语言实现类似tree命令的小程序

    tree 命令是一个小型的跨平台命令行程序,用于递归地以树状格式列出或显示目录的内容。本文将通过Go语言实现类似tree命令的小程序,需要的可以参考一下
    2022-10-10
  • Golang的select多路复用及channel使用操作

    Golang的select多路复用及channel使用操作

    这篇文章主要介绍了Golang的select多路复用及channel使用操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12

最新评论