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.Map
, sync.Map
和 map
不同,不是以语言原生形态提供,而是在 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
会有更好的性能。