go学习(10)并发读写访问map问题

Golang 里面 map 不是并发安全的,这一点是众所周知的,而且官方文档也很早就给了解释:Why are map operations not defined to be atomic?. 也正如这个解释说的一样,要实现一个并发安全的 map 其实非常简单。

并发安全

实际上,大多数情况下,对一个 map 的访问都是读操作多于写操作,而且读的时候,是可以共享的。所以这种场景下,用一个 sync.RWMutex 保护一下就是很好的选择:

type syncMap struct {

items map[string]interface{}

sync.RWMutex

}

上面这个结构体定义了一个并发安全的 string map,用一个 map 来保存数据,一个读写锁来保护安全。这个 map 可以被任意多的 goroutine 同时读,但是写的时候,会阻塞其他读写操作。添加上 GetSetDelete 等方法,这个设计是能够工作的,而且大多数时候能表现不错。

但是这种设计会有些性能隐患。主要是两个方面:

  1. 读写锁的粒度太大了,保护了整个 map 的访问。写操作是阻塞的,此时其他任何读操作都无法进行。
  2. 如果内部的 map 存储了很多 key,GC 的时候就需要扫描很久。

「分表」

一种解决思路是“分表”存储,具体实现就是,基于上面的 syncMap 再包装一次,用多个 syncMap 来模拟实现一个 map:

type SyncMap struct {

shardCount uint8

shards []*syncMap

}

上面这种设计用了一个 *syncMap 的 slice 来保存数据,shardCount 提供了分表量的可定制性。实际上 shards 同样可以实现为 map[string]*syncMap

在这种设计下,数据(key:value)会被分散到不同的 syncMap,而每个 syncMap 又有自己底层的 map。数据分散了,锁也分散了,能够很大程度上提高随机访问性能。而且在数据量大、高并发、写操作频繁的场景下,这种提升会更加明显。

那么数据如何被分配到指定的分块呢?一种很通用也很简单的方法就是 hash. 字符串的哈希算法有很多,byvoid 大神实现和比较了多种字符串 hash 函数(各种字符串Hash函数比较),得出结论是:“BKDRHash无论是在实际效果还是编码实现中,效果都是最突出的”。这里采用了 BKDRHash 来实现:​​​​​​​

const seed uint32 = 131 // 31 131 1313 13131 131313 etc..


func bkdrHash(str string) uint32 {

var h uint32


for _, c := range str {

h = h*seed + uint32(c)

}


return h

}


// Find the specific shard with the given key

func (m *SyncMap) locate(key string) *syncMap {

return m.shards[bkdrHash(key)&uint32((m.shardCount-1))]

}

locate 方法调用 bkdrHash 函数计算一个 key 的哈希值,然后用该值对分表量取模得到在 slice 的 index,之后就能定位到对应的 syncMap.

这种实现足够简单,而且也有不错的性能表现。除了基本的 GetSetDelete 等基本操作之外,迭代(range)功能也非常有用。更多的功能和细节,都可以在源码里找到答案: https://github.com/DeanThompson/syncmap.

还有一点:

如果业务场景能保证我们绝不会同时读写一个key的话也不用加锁,一个小例子试验下。。

会有冲突的情况:

package main


func main() {

Map := make(map[int]int)


for i := 0; i < 100; i++ {

go writeMap(Map, i, i)

go readMap(Map, i)

}


}


func readMap(Map map[int]int, key int) int {

return Map[key]

}


func writeMap(Map map[int]int, key int, value int) {

Map[key] = value

}

go run -race main.go 结果如下:

那么稍微改下呢。。

package main


func main() {

Map := make(map[int]int)


for i := 0; i < 2; i++ {

go writeMap(Map, i, i)

go readMap(Map, (i+1) % 2)

}


}


func readMap(Map map[int]int, key int) int {

return Map[key]

}


func writeMap(Map map[int]int, key int, value int) {

Map[key] = value

}

出现的是不冲突,但是没啥意义,因为几乎没有这样的应用场景吧,在这做这个小测试只是想说明golang 的map会出现读写冲突是因为map是引用类型,同时读写共享资源会使得共享资源崩溃

猜你喜欢

转载自blog.csdn.net/ltk80/article/details/82919625