关于go:Go-语言-map-是并发安全的吗

44次阅读

共计 4102 个字符,预计需要花费 11 分钟才能阅读完成。

原文链接: 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 的并发安全性,从而在性能和安全性之间做出衡量。

如何并发平安

接下来介绍三种并发平安的形式:

  1. 读写锁
  2. 分片加锁
  3. 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

定义了两个办法:ReadMapWriteMap。在 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 的锁数组。

定义了两个办法:ReadMapWriteMap。在这两个办法中,咱们都应用了一个 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,它通过将读写拆散的形式实现了某些特定场景下的性能晋升。

以上就是本文的全部内容,如果感觉还不错的话欢送 点赞 转发 关注,感激反对。


参考文章:

  • https://zhuanlan.zhihu.com/p/356739568

举荐浏览:

  • Go 语言切片是如何扩容的?
  • Go 语言数组和切片的区别
  • Go 语言 new 和 make 关键字的区别
  • 为什么 Go 不反对 []T 转换为 []interface
  • 为什么 Go 语言 struct 要应用 tags

正文完
 0