go中的sync.Map_sync.map 找不到数据-程序员宅基地

技术标签: go  源码学习  sync  Map  

Go 1.9中的sync.Map提供了线程安全的map,它的优点总结如下:(网上找的)

1.空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
2.使用只读数据(read),避免读写冲突。
3.动态调整,miss次数多了之后,将dirty数据提升为read。
4.double-checking。
5.延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。

6.优先从read读取、更新、删除,因为对read的读取不需要锁。

其实当我们读完它内部实现后就能很好地理解以上好处了。

先从Map的数据结构看起

type Map struct {
	mu Mutex
	read atomic.Value // readOnly
	dirty map[interface{}]*entry
	misses int
}

Mutex加锁的工具,read一个只读的数据结构,所以对它的读总是线程安全的。dirty是map结构,它包含整个Map中的最新的entries,它不是线程安全的,为了保证多线程安全的情况下操作它,需要对它加锁;当dirty为空时,下一次写操作会复制read字段中未删除的数据到dirty。misses计数器,当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一, 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。

    其中read atomic.Value中存的是readOnly数据结构,我们来看下readOnly

type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
}

m无非存数据,amended为true的话说明,dirty数组包含一些新的entries,却存没有在readOnly中。

因为这里数据是只读的,所以读数据时候优先读read数据,若read中没有,则去dirty中读,读到则misses加一。

Map中还一个expunged指针,指向被删除的键值对。Map中的entry结构体无非存有指向value的指针

var expunged = unsafe.Pointer(new(interface{}))

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
	p unsafe.Pointer // *interface{}
}

既然是Map,那我们先看下如何存数据,Store(key, value interfaces{})

func (m *Map) Store(key, value interface{}) {
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			// The entry was previously expunged, which implies that there is a
			// non-nil dirty map and this entry is not in it.
			m.dirty[key] = e
		}
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		e.storeLocked(&value)
	} else {
		if !read.amended {
			// We're adding the first new key to the dirty map.
			// Make sure it is allocated and mark the read-only map as incomplete.
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

先看这个情况:第一次new() Map后,里面肯定没有任何数据,第一次Store传入键值对到Map中。

首先通过atomic.Value的Load方法,加载readOnly到read中,此时read中肯定没有任何数据,于是我们要从dirty中取数据,由于它不是线程安全,所以调用mutex锁工具lock,于是读dirty中,也找不到,则判断下amended是否为false(确保是不是第一次往dirty中加数据),由于readOnly是刚创建的其ammended值为false,所以此时方法调用进入m.dirtyLocked()

func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}

	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[interface{}]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}

该方法创建dirty,把readOnly的(未被删除的)键值对复制到dirty中,此时readOnly为空。

func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := atomic.LoadPointer(&e.p)
	for p == nil {
		if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
	}
	return p == expunged
}

创建完dirty后,会创建新的readOnly赋值给read,此时amended为true;最后在dirty中添加键值对,解锁。

情况二:如果此时并不是第一次,并且一开始就在readOnly中找到。

此时调用e.tryStore(&Value);

func (e *entry) tryStore(i *interface{}) bool {
	p := atomic.LoadPointer(&e.p)
	if p == expunged {
		return false
	}
	for {
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
	}
}

先判断取到的value是不是已经被删除的,如果是则return false;否则,调用cas替换原value的地址值指向新的value,则完成对read中的键值对的更新。如果在更新的过程中,value值被删除则return false;

情况三:如果在read中没找到,在dirty中找到,则加锁,加锁后还是要再load read(以防此时dirty晋升为read),从read中读到被标记删除的value则cas更新dirty中对应key的value设为nil,最后调用storePionter更改value的值。

func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

我们再来看Load()方法吧,读数据。

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		// Avoid reporting a spurious miss if m.dirty got promoted while we were
		// blocked on m.mu. (If further loads of the same key will not miss, it's
		// not worth copying the dirty map for this key.)
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			e, ok = m.dirty[key]
			// Regardless of whether the entry was present, record a miss: this key
			// will take the slow path until the dirty map is promoted to the read
			// map.
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}
老样子,先从read中找,如果找到则返回value对应的数据。如果没找到,需要判断 amended是否为true,false的话说明dirty中没有多余数据,那找不到。如果为true,则去dirty中找,操作dirty需要先加锁,先判断read中有无(防止dirty晋升成read),找到返回,没找到再找dirty,如果找到那么给misses加一。
func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}
如果misses长度大于等于dirty的长度,则晋升dirty为read,这步骤复杂度为O1,因为无非指针替换,再把dirty更新为nil,misses更新为0;

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/panxj856856/article/details/80363808

智能推荐

Python综合评价模型(二)灰色关联度法_灰色关联度分析b站用户价值评估模型怎么做-程序员宅基地

文章浏览阅读2.3k次,点赞4次,收藏45次。Python综合评价模型(二)灰色关联度法_灰色关联度分析b站用户价值评估模型怎么做

vue添加Electron在渲染进程调用ipc报错:__dirname is not defined_electron __dirname is not defined-程序员宅基地

文章浏览阅读1w次,点赞5次,收藏6次。错误详情使用vue创建项目并添加vue-cli-plugin-electron-builder插件后在渲染进程中调用ipc与主进程通讯报错:__dirname is not defined错误原因As of v2.0 of VCPEB, Electron nodeIntegration is disabled by default. This blocks all node APIs such as require. This reduces security risks, and is a rec_electron __dirname is not defined

硬件基础集线器、交换机、路由器原理_二层交换机和集线器-程序员宅基地

文章浏览阅读766次,点赞15次,收藏17次。OSI (Open System Interconnect)模型全称为开放式通信系统互连参考模型,是国际标准化组织 ( ISO ) 提出的一个试图使各种计算机在世界范围内互连为网络的标准框架OSI将计算机网络体系结构划分为七层,每一层实现各自的功能和协议,并完成与相邻层的接口通信。即每一层扮演固定的角色,互不打扰集线器的主要功能是对接收到的信号进行再生整形放大,以扩大网络的传输距离,同时把所有节点集中在以它为中心的节点上。它工作于OSI(开放系统互联参考模型)参考模型第一层(物理层)。_二层交换机和集线器

Java多线程下载并具断点续传功能JAR_断点续传 开源jar-程序员宅基地

文章浏览阅读1.7w次。这两天把原来写的多线程下载程序整理了一下,考虑到原来的都是散文件,使用起来也不方便,所以决定把其写JAR,这样,使用起来也方便。并且增加使用XML保存下载文件以便下次再次下载,也修正了原来的一些BUG,只要你的电脑允许,想同时有多少个下载就有多少个下载。 这里我有一个示例,因为这里用到了JDOM处理XML文件,本来是想把用到的JDOM也把包到downFile.jar(下载地址:ht_断点续传 开源jar

mybatis-generator-maven-plugin插件使用-程序员宅基地

文章浏览阅读1.2w次,点赞3次,收藏13次。该插件用于连接数据库自动生成Mybatis需要的代码环境:springboot,idea,jdk8搭建springboot项目:idea->file->new->Spring initialzr->next->进入配置:java Version选择8,其余可以默认,next->需要提那就的依赖(需要的功能),默认可以不添加。后续需要去pom.xml配置即可->next->finish。进入项目,点击pom.xml,添加插件。(此处包括my_mybatis-generator-maven-plugin

【卡梅德生物】蛋白表达之酵母表达载体-程序员宅基地

文章浏览阅读424次,点赞10次,收藏7次。首先,酵母细胞是一种真核细胞,与人类细胞相似,因此酵母表达系统可以用于生产与人类相关的蛋白质,酵母载体是指将外源基因表达于酵母细胞中的一种工具。

随便推点

数组-程序员宅基地

文章浏览阅读132次。■ 概述  数组,就是一个集合,里面存放了相同类型的数据元素■特点  1) 数组中的每个数据元素都是相同的数据类型  2) 数组是由连续的内存位置组成,数组里的数据放在一块连续的内存空间■一维数组□ 一维数组定义的三种方式  1) 数据类型 数组名[数组长度];int arr[5];arr[0] = 10;arr[1] = 20;arr[2] = 30;arr[3]..._编程程序数组 int[] arr = {10,20,-5,6,9,6,8,7,2,33,1}的最大值

互联网+下的慧算账体验式营销-程序员宅基地

文章浏览阅读357次。本文讲的是互联网+下的慧算账体验式营销,互联网+概念如火如荼,面对新一轮的行业变革洗礼,代理记账机构已经深刻的认识到企业转型和创新是其长期持续发展的必然方向。通过高效的代账工具,增加企业内部驱动力,从而进一步提升工作效益、降低成本是必要的手段。  就互联网化的代记账工具而言,“免费”和“用户至上”无疑是核心精神,然而,财税服务行业不同于一般的领域,因为..._慧算账营销方式

SDL编程入门(25)限制帧率_sdl_setticks-程序员宅基地

文章浏览阅读1.5k次。限制帧率我们可以用SDL定时器做的另一件事是手动限制帧率。在这里,我们将禁用vsync,并保持最高帧率。//屏幕尺寸常量const int SCREEN_WIDTH = 640;const int SCREEN_HEIGHT = 480;const int SCREEN_FPS = 60;const int SCREEN_TICKS_PER_FRAME = 1000 / SCREEN_FPS;在这个演示中,我们将正常渲染我们的帧,但在帧的最后,我们将等待,直到帧时间完成。例如这里,当你想以6_sdl_setticks

Linux下的uml工具_linux uml-程序员宅基地

文章浏览阅读1.1w次。1.DIA ubuntu下推荐的,和viso的风格以及操作比较接近,但画出来的东西感觉不怎么美观,手工修改显示组件的属性会得到好一些的效果。2.yed 和其他的UML工具的使用方式有较大差别,因为它只有一个组件,鼠标点下去就会产生一个,通过修改它的属性,可以得到各种图形,默认的type有:image,UMLNode,UML,Shape,每个类别下的属性都很多,可以满足日常工作需要。只_linux uml

Writing an LLVM Pass_writing pass on llvm17-程序员宅基地

文章浏览阅读2.1k次,点赞2次,收藏8次。目录1.简介 - 什么是pass?2.快速入门 - 写下hello world3.设置构建环境4.Basic code required5.Pass类和要求6.ImmutablePass类7.ModulePass类8.CallGraphSCCPass类9.FunctionPass类10.doInitialization(Module&)方法11.L..._writing pass on llvm17

linux最低配置版本,Linux各个版本配置要求-程序员宅基地

文章浏览阅读4.8k次。桌面版的Linux越来越受到用户的欢迎, 它的画面也越来越精细。但是它所需要的配置不像windows那样来得那么夸张,到底linux的配置要求是什么样的呢?答案是:不需要太高的配置。让我们来看看将于本周发布的Ubuntu9.04,官方推荐的最低配置为700MHz处理器和256MB内存,而我认为这配置很低。根据我的经验,你可以在那个缓慢的处理器上运行GNOME 2.26,但是内存容量实在是太低了。你..._linuxptp最低版本要求

推荐文章

热门文章

相关标签