Go基础学习07-map注意事项;多协程对map的资源竞争;sync.Mutex避免竟态条件
文章目录
- Go中map使用以及注意事项
- map使用时的并发安全问题
Go中map使用以及注意事项
Go语言中map使用简单示例:
func main() {
var mp map[string]int
// mp := map[string]int{}
val, ok := mp["one"]
if ok {
fmt.Println(val)
} else {
fmt.Println(val)
}
mp["two"] = 10
}
思考一:Go语言的map的键类型为什么不能是函数类型、字典类型、切片类型:
相信学习过其他语言比如Java语言的朋友对于Java中的HashMap都比较熟悉,对于所有键值对元素的数据结构来说,存储过程先计算key的哈希值,随后使用哈希值定位到对应的bucket,将key和value作为元素存储到对应的bucket中。在这个过程中可能会发生哈希碰撞,在Java中对于哈希碰撞常见的解决方案有拉链法,由于哈希碰撞的存在对于哈希表的键的要求,不仅仅能够计算哈希值,同时能够对键进行判等操作(解决哈希碰撞时key重复的问题)。对于Go语言的map的实现仍然如此,需要考虑到哈希碰撞的出现,所以就要求key必须能够进行判等操作。
如果go的map的键为上述三种类型,在运行时就会发生panic。
思考二:Go语言中应该优先考虑那些类型作为map的键的类型
只从性能的角度出发,而不考虑上下文时:在map中比较耗时的操作主要出现在连个地方:
- 把键值转换为哈希值
- 把要查找的键值与哈希桶中的键值做对比
对于所有的基本类型、指针类型,以及数组类型、结构体类型和接口类型,Go 语言都有一套算法与之对应。这套算法中就包含了哈希和判等。
- 以求哈希的操作为例,宽度越小的类型速度通常越快。对于布尔类型、整数类型、浮点数类型、复数类型和指针类型来说都是如此。对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。
- 类型的宽度是指它的单个值需要占用的字节数。比如,bool、int8和uint8类型的一个值需要占用的字节数都是1,因此这些类型的宽度就都是1。
- 高级类型如:数组类型的值求哈希实际上是依次求得它的每个元素的哈希值并进行合并,所以速度就取决于它的元素类型以及它的长度。细则同上。
- 结构体类型:哈希实际上就是对它的所有字段值求哈希并进行合并,关键在于它的各个字段的类型以及字段的数量。
优先选用数值类型和指针类型,通常情况下类型的宽度越小越好。如果非要选择字符串类型的话,最好对键值的长度进行额外的约束。
map使用时的并发安全问题
考虑如下代码:
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)
}
}
开启两个协程并发对map进行读写操作,上述代码运行直接报如下错误:
goroutine 6 [running]:
main.read(0x0?)
/home/wt/Backend/go/goprojects/src/golearndetail/go36/learn09/demo02.go:20 +0x2d
created by main.main
/home/wt/Backend/go/goprojects/src/golearndetail/go36/learn09/demo02.go:12 +0xa5
goroutine 1 [sleep]:
time.Sleep(0x6fc23ac00)
/usr/local/go/src/runtime/time.go:195 +0x135
main.main()
/home/wt/Backend/go/goprojects/src/golearndetail/go36/learn09/demo02.go:15 +0xfb
goroutine 7 [sleep]:
time.Sleep(0x1)
/usr/local/go/src/runtime/time.go:195 +0x135
main.write(0x0?)
/home/wt/Backend/go/goprojects/src/golearndetail/go36/learn09/demo02.go:27 +0x25
created by main.main
/home/wt/Backend/go/goprojects/src/golearndetail/go36/learn09/demo02.go:14 +0xec
Process finished with the exit code 2
对于map的读写是非原子性操作,存在资源竞争,不是现成安全的,可以使用如下命令检测:
go run race ...
为了将非并发安全的读取操作更改为并发安全的,可以引入sync.Mutex,在读、写操作前进行加锁操作;操作后进行解锁,保证并发安全。
func main() {
m := map[int]string{
1: "haha",
}
var mutex sync.Mutex // 创建一个互斥锁
// 启动读协程
go read(m, &mutex)
// 等待一秒钟,确保读协程已经开始运行
time.Sleep(time.Second)
// 启动写协程
go write(m, &mutex)
// 等待足够长的时间,以便读写协程可以运行
time.Sleep(30 * time.Second)
// 打印最终的map
fmt.Println(m)
}
func read(m map[int]string, mutex *sync.Mutex) {
for {
mutex.Lock() // 在读取之前加锁
value := m[1]
mutex.Unlock() // 读取完毕后解锁
fmt.Println("Read:", value)
time.Sleep(1 * time.Second)
}
}
func write(m map[int]string, mutex *sync.Mutex) {
for {
mutex.Lock() // 在写入之前加锁
m[1] = "write"
mutex.Unlock() // 写入完毕后解锁
fmt.Println("Write:", m[1])
time.Sleep(1 * time.Second)
}
}
后续会将sync.Mutex的底层原理进行总结展示。