目录

Go并发编程实战课笔记—Map

Go并发编程实战课笔记—Map

以下为鸟窝大佬的Go 并发编程实战课 中摘录的笔记

https://img.zhengyua.cn/20210220105139.png

基本使用方法

Go内建的map类型如下:

1
map[k]v
  • Key类型的K必须是可比较的

Golang中布尔型、整数、浮点数、复数、字符串、指针、Channel、接口都是可比较的,包含可比较元素的struct和数组,而slice、map、函数值都是不可比较的。

  • 若使用struct类型做key,则需要保证struct逻辑不变;
  • Map[key]返回结果可以是一个值或者两个值;
  • map无序的,遍历时迭代的元素顺序是不确定的;

若想保证元素有序则可以使用辅助的数据结构ordermap。

常见错误

未初始化

在使用map对象之前必须初始化,否则会直接panic。

并发读写

Go内建的map对象不是线程安全的,并发读写的时候运行时会检查,遇到并发问题时会导致panic。

实现线程安全的map类型

为了实现线程安全的map类型一般常见的方案有两种,分别是:

  • 读写锁
  • 分片加锁

在追求更高性能时分片加锁更好,能够降低锁的粒度来提高此map对象的吞吐,若并发性能要求不高的场景简单加锁更简单。

读写锁

利用内置读写锁的方式来使map对象使用时保证线程安全:

1
2
3
4
type RWMap struct{
  sync.RWMutex
  m map[int]int
}

分片加锁

在并发编程中,我们会尽量减少锁的使用,因为在大量并发读写情况下锁的竞争会非常激烈,锁时性能下降的万恶之源之一。

所以我们会尽量减少锁的粒度和锁的持有时间,而减少锁的力度常用的方法就是分片,即将一把锁分成几把锁,每一个锁控制一个分片。

在golang中比较著名的分片并发实现时orcaman/concurrent-map。

它默认采用32个分片,GetShard是一个关键的方法,能够根据key计算出分片索引。

增加或者查询的时候首选根据分片索引来得到分片对象,然后对分片对象加锁进行操作。

sync.Map

Sync.Map并不是用来替换内建的map类型,它只能被应用到一些特殊的场景里,比如在以下场景中使用该类型比加锁的方式性能要更好:

  • 只会增长的缓存系统中,一个key只写入一次而被读很多次;
  • 多个goroutine为不相交的键集读、写和重写键值对;

一般建议是针对于自己的实际场景做性能评测,如果确实能够显著提高性能,再使用sync.Map。

实现

主要优化点如下:

  • 空间换时间。通过冗余的两个数据结构(只读的read字段、可写的dirty字段),来减少锁对性能的影响,对只读字段的操作不需要加锁;
  • 优先从read字段读取、更新、删除,因为对read字段的读取不需要锁;
  • 动态调整。miss次数达到某值将dirty数据提升为read,避免总是从dirty中加锁读取;
  • Double-checking。加锁之后先还要检查read字段,确定真的不存在才操作dirty字段;
  • 延迟删除。删除一个键值只是打标记,只有在提升dirty字段为read字段的时候才清理删除的数据;

具体可以查看相关的源码理解。