Go并发编程实战课笔记—Map
目录
Go并发编程实战课笔记—Map
以下为鸟窝大佬的Go 并发编程实战课 中摘录的笔记
基本使用方法
Go内建的map类型如下:
|
|
- Key类型的K必须是可比较的;
Golang中布尔型、整数、浮点数、复数、字符串、指针、Channel、接口都是可比较的,包含可比较元素的struct和数组,而slice、map、函数值都是不可比较的。
- 若使用struct类型做key,则需要保证struct逻辑不变;
- Map[key]返回结果可以是一个值或者两个值;
- map无序的,遍历时迭代的元素顺序是不确定的;
若想保证元素有序则可以使用辅助的数据结构ordermap。
常见错误
未初始化
在使用map对象之前必须初始化,否则会直接panic。
并发读写
Go内建的map对象不是线程安全的,并发读写的时候运行时会检查,遇到并发问题时会导致panic。
实现线程安全的map类型
为了实现线程安全的map类型一般常见的方案有两种,分别是:
- 读写锁
- 分片加锁
在追求更高性能时分片加锁更好,能够降低锁的粒度来提高此map对象的吞吐,若并发性能要求不高的场景简单加锁更简单。
读写锁
利用内置读写锁的方式来使map对象使用时保证线程安全:
|
|
分片加锁
在并发编程中,我们会尽量减少锁的使用,因为在大量并发读写情况下锁的竞争会非常激烈,锁时性能下降的万恶之源之一。
所以我们会尽量减少锁的粒度和锁的持有时间,而减少锁的力度常用的方法就是分片,即将一把锁分成几把锁,每一个锁控制一个分片。
在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字段的时候才清理删除的数据;
具体可以查看相关的源码理解。