Go 学习笔记(67)— Go 并发安全字典 sync.Map

1. 并发不安全的 map

Go 语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

换句话说,在同一时间段内,让不同 goroutine 中的代码,对同一个字典进行读写操作是不安全的。字典值本身可能会因这些操作而产生混乱,相关的程序也可能会因此发生不可预知的问题。

package main

import (
	"fmt"
	"time"
)

func main() {
	m := map[int]string{
		1: "haha",
	}

	go read(m)
	time.Sleep(time.Second)
	go write(m)
	time.Sleep(30 * time.Second)
	fmt.Println(m)
}

func read(m map[int]string) {
	for {
		_ = m[1]
		time.Sleep(1)
	}
}

func write(m map[int]string) {
	for {
		m[1] = "write"
		time.Sleep(1)
	}
}

执行一段时间后会报错:

fatal error: concurrent map read and map write

2. 并发安全字典 sync.Map

需要并发读写时,一般的做法是加锁,但这样性能并不高, Go 语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Mapsync.Mapmap 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用, Store 表示存储, Load 表示获取, Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值, Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true ,终止迭代遍历时,返回 false
  • Store:存储一对 key-value 值。
  • Load:根据 key 获取对应的 value 值,并且可以判断 key 是否存在。

  • LoadOrStore:如果 key 对应的 value 存在,则返回该 value;如果不存在,存储相应的 value。

  • Delete:删除一个 key-value 键值对。

  • Range:循环迭代 sync.Map,效果与 for range 一样。

它所有的方法涉及的键和值的类型都是 interface{} ,也就是空接口,这意味着可以包罗万象。所以,我们必须在程序中自行保证它的键类型和值类型的正确性。

并发安全的 sync.Map 演示代码如下:

package main

import (
      "fmt"
      "sync"
)

func main() {
    
	// 声明 scene,类型为 sync.Map,注意,sync.Map 不能使用 make 创建。
    var scene sync.Map

    // 将键值对保存到sync.Map
    // sync.Map 将键和值以 interface{} 类型进行保存。
    scene.Store("greece", 97)
    scene.Store("london", 100)
    scene.Store("egypt", 200)

    // 从sync.Map中根据键取值
    fmt.Println(scene.Load("london"))

    // 根据键删除对应的键值对
    scene.Delete("london")

    // 遍历所有sync.Map中的键值对
    // 遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{},
    // 每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回。
    scene.Range(func(k, v interface{}) bool {

        fmt.Println("iterate:", k, v)
        return true
    })

}

输出结果:

100 true
iterate: greece 97
iterate: egypt 200

sync.Map 键的实际类型不能是函数类型、字典类型和切片类型。由于这些键值的实际类型只有在程序运行期间才能够确定,所以 Go 语言编译器是无法在编译期对它们进行检查的,不正确的键值实际类型肯定会引发 panic

3. 如何保证并发安全字典中的键和值的类型正确性?

3.1 让并发安全字典只能存储某个特定类型的键。

比如指定这里的键只能是 int 类型的,或者只能是字符串,又或是某类结构体。一旦完全确定了键的类型,你就可以在进行存、取、删操作的时候,使用类型断言表达式去对键的类型做检查了。

一般情况下,这种检查并不繁琐。而且,你要是把并发安全字典封装在一个结构体类型里面,那就更加方便了。你这时完全可以让 Go 语言编译器帮助你做类型检查。

package main

import (
	"fmt"
	"sync"
)

// IntStrMap 代表键类型为int、值类型为string的并发安全字典。
type IntStrMap struct {
	m sync.Map
}

func (iMap *IntStrMap) Delete(key int) {
	iMap.m.Delete(key)
}

func (iMap *IntStrMap) Load(key int) (value string, ok bool) {
	v, ok := iMap.m.Load(key)
	if v != nil {
		value = v.(string)
	}
	return
}

func (iMap *IntStrMap) LoadOrStore(key int, value string) (actual string, loaded bool) {
	a, loaded := iMap.m.LoadOrStore(key, value)
	actual = a.(string)
	return
}

func (iMap *IntStrMap) Range(f func(key int, value string) bool) {
	f1 := func(key, value interface{}) bool {
		return f(key.(int), value.(string))
	}
	iMap.m.Range(f1)
}

func (iMap *IntStrMap) Store(key int, value string) {
	iMap.m.Store(key, value)
}

// pairs 代表测试用的键值对列表。
var pairs = []struct {
	k int
	v string
}{
	{k: 1, v: "a"},
	{k: 2, v: "b"},
	{k: 3, v: "c"},
	{k: 4, v: "d"},
}

func main() {

	var iMap IntStrMap
	iMap.Store(pairs[0].k, pairs[0].v)
	iMap.Store(pairs[1].k, pairs[1].v)
	iMap.Store(pairs[2].k, pairs[2].v)
	fmt.Println("[Three pairs have been stored in the IntStrMap instance]")

	iMap.Range(func(key int, value string) bool {
		fmt.Printf("The result of an iteration in Range: %d, %s\n", key, value)
		return true
	})

	k0 := pairs[0].k
	v0, ok := iMap.Load(k0)
	fmt.Printf("The result of Load: %v, %v (key: %v)\n", v0, ok, k0)

	k3 := pairs[3].k
	v3, ok := iMap.Load(k3)
	fmt.Printf("The result of Load: %v, %v (key: %v)\n", v3, ok, k3)

	k2, v2 := pairs[2].k, pairs[2].v
	actual2, loaded2 := iMap.LoadOrStore(k2, v2)
	fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
		actual2, loaded2, k2, v2)

	v3 = pairs[3].v
	actual3, loaded3 := iMap.LoadOrStore(k3, v3)
	fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
		actual3, loaded3, k3, v3)

	k1 := pairs[1].k
	iMap.Delete(k1)
	fmt.Printf("[The pair with the key of %v has been removed from the IntStrMap instance]\n", k1)

	v1, ok := iMap.Load(k1)
	fmt.Printf("The result of Load: %v, %v (key: %v)\n", v1, ok, k1)

	v1 = pairs[1].v
	actual1, loaded1 := iMap.LoadOrStore(k1, v1)
	fmt.Printf("The result of LoadOrStore: %v, %v (key: %v, value: %v)\n",
		actual1, loaded1, k1, v1)

	iMap.Range(func(key int, value string) bool {
		fmt.Printf("The result of an iteration in Range: %d, %s\n", key, value)
		return true
	})

	fmt.Println()
}

sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量。

sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值