1.Go语言学习(2)--map的底层原理
2.golang map 源码解读(8问)
3.Go|map底层实现、扩容规则、特性
4.Go语言sync.Map实现原理
5.Go实例讲解,并发编程-map并发读写的线程安全性问题
6.Go 语言入门 2-集合(map)的特性及实现原理
Go语言学习(2)--map的底层原理
Golang的Map底层是通过HashTable实现的,创建map时实际返回的是runtime/map.go中hmap对象的指针。hmap中buckets指向的通达信阴阳脸指标公式源码是bucket数组的指针,bucket数组大小由B决定,通常为2^B个。单个bucket结构体内部不直接定义keys、values和overflow,而是通过指针运算访问。
在查找、插入和删除过程中,通过哈希函数将键转换为哈希值,然后使用哈希值对bucket进行定位。查找时直接访问哈希表中对应的bucket,插入和删除操作涉及更新bucket中的键值对。
Map的扩容机制基于负载因子,负载因子用于衡量冲突情况,定义为bucket数量与键值对数量的比值。当负载因子大于6.5,或者overflow数量超过时,Map会触发扩容。扩容时,新bucket长度为原bucket长度的2倍,旧bucket数据搬迁到新bucket。为了减少一次性搬迁带来的延迟,Go采用逐步搬迁策略,每次访问map时触发搬迁,每次搬迁2个键值对。
扩容后,新bucket存储新插入的键值对,老bucket中的键值对逐步搬迁到新bucket。搬迁完成后,删除老bucket。搬迁过程中,老bucket中的键值对将位于新bucket的前部,新插入的webug3.0源码键值对位于新bucket的后部。
等量扩容是重新组织bucket,提升bucket的使用率,而不是简单地增加容量。在某些极端场景下,如果键值对集中在少数bucket,可能导致overflow的bucket数量增多,但负载因子不高,无法执行增量搬迁。这时进行一次等量扩容,可以减少overflow的bucket数量,优化访问效率。
golang map 源码解读(8问)
map底层数据结构为hmap,包含以下几个关键部分:
1. buckets - 指向桶数组的指针,存储键值对。
2. count - 记录key的数量。
3. B - 桶的数量的对数值,用于计算增量扩容。
4. noverflow - 溢出桶的数量,用于等量扩容。
5. hash0 - hash随机值,增加hash值的随机性,减少碰撞。
6. oldbuckets - 扩容过程中的旧桶指针,判断桶是否在扩容中。
7. nevacuate - 扩容进度值,小于此值的已经完成扩容。
8. flags - 标记位,用于迭代或写操作时检测并发场景。
每个桶数据结构bmap包含8个key和8个value,以及8个tophash值,用于第一次比对。
overflow指向下一个桶,桶与桶形成链表存储key-value。
结构示意图在此。
map的初始化分为3种,具体调用的函数根据map的初始长度确定:
1. makemap_small - 当长度不大于8时,只创建hmap,淘宝 crm源码下载不初始化buckets。
2. makemap - 当长度参数为int时,底层调用makemap。
3. makemap - 初始化hash0,计算对数B,并初始化buckets。
map查询底层调用mapaccess1或mapaccess2,前者无key是否存在的bool值,后者有。
查询过程:计算key的hash值,与低B位取&确定桶位置,获取tophash值,比对tophash,相同则比对key,获得value,否则继续寻找,直至返回0值。
map新增调用mapassign,步骤包括计算hash值,确定桶位置,比对tophash和key值,插入元素。
map的扩容有两种情况:当count/B大于6.5时进行增量扩容,容量翻倍,渐进式完成,每次最多2个bucket;当count/B小于6.5且noverflow大于时进行等量扩容,容量不变,但分配新bucket数组。
map删除元素通过mapdelete实现,查找key,计算hash,找到桶,遍历元素比对tophash和key,找到后置key,value为nil,修改tophash为1。
map遍历是无序的,依赖mapiterinit和mapiternext,代源码下载器选择一个bucket和offset进行随机遍历。
在迭代过程中,可以通过修改元素的key,value为nil,设置tophash为1来删除元素,不会影响遍历的顺序。
Go|map底层实现、扩容规则、特性
Go语言中的map数据结构底层实现是基于哈希表,其中关键要点包括哈希函数、桶的分配以及扩容规则。哈希表通过hash函数将键值对分布到桶中,Go语言要求桶的数量m必须是2的整数次幂,以确保所有桶均匀被选中,避免空桶问题。负载因子(元素数量除以桶的数量)是衡量map负载状态的指标。
Go的map底层结构包括指向bmap(哈希表节点)和mapextra(溢出桶信息)的指针。在扩容时,Go采用渐进式策略,每次只迁移2个桶,以减少对性能的影响。当负载因子超过6.5时,会触发翻倍扩容,将桶数量翻倍。等量扩容则在overflow桶过多且负载因子不高时发生,创建与旧桶数量相同的新的桶并重新排列键值对。
值得注意的是,Go map的特性包括遍历无序,由于语言设计的随机化,range遍历结果的顺序可能会变化,建议按需排序。此外,Go map是非线程安全的,官方推荐在并发场景下使用读写锁,或者考虑使用sync.Map,它为读写操作提供了更高的并发性能,但写入密集型场景可能会降低效率。
Go语言sync.Map实现原理
Go语言的销售登记网站源码sync.Map是并发安全的map类型,它在Go 1.9版本引入,解决并发读写问题时无需加锁,通过read和dirty两个map实现读写分离,提升效率。
sync.Map的核心设计思想为“空间换时间”,利用冗余的数据结构减少锁的使用。read和dirty这两个map分别存放key-entry,entry指向value。read和dirty中key指向同一个value,改动entry时read和dirty会自动更新。
实现原理中,read作为并发读取安全的区域,dirty作为写入区域,通过锁机制和双检查机制确保操作正确。双检查机制在需要修改dirty前上锁,防止read中数据在检查过程中改变。延迟删除机制将删除操作标记,避免立即耗时,减少并发冲突。
read优先原则是当需要执行读、删除、更新操作时,先在read中执行,更快且更安全,若read中无法获取结果则尝试dirty。状态机机制通过entry的指针p表示三种状态,协助同步和管理数据。
sync.Map内部结构包括:readOnly、entry和sync.Map结构体。readOnly结构用于并发读取,entry数据结构用于存储指向value的指针,entry状态有三种:nil、expunged和正常。
源码解析揭示了Load、Store、Delete和Range等方法实现细节。Load方法在read map未命中时尝试dirty map,Store方法在dirty map与read map数据同步后添加新键值对,Delete方法在amended为true时,将dirty提升为read,并遍历read进行删除操作。数据迁移策略在miss计数达到一定阈值时将dirty同步到read,减少并发操作开销。
使用sync.Map时,无需担心传统并发操作的锁竞争问题,通过优化的数据结构和操作策略显著提升并发读写性能。推荐在读多写少的场景下使用sync.Map,实现高效、安全的并发管理。
Go实例讲解,并发编程-map并发读写的线程安全性问题
先上实例代码,后面再来详细讲解。
/** * 并发编程,map的线程安全性问题,使用互斥锁的方式 */ package main import ( "sync" "time" "fmt" ) var data map[int]int = make(map[int]int) var wgMap sync.WaitGroup = sync.WaitGroup{ } var muMap sync.Mutex = sync.Mutex{ } func main() { // 并发启动的协程数量 max := wgMap.Add(max) time1 := time.Now().UnixNano() for i := 0; i < max; i++ { go modifySafe(i) } wgMap.Wait() time2 := time.Now().UnixNano() fmt.Printf("data len=%d, time=%d", len(data), (time2-time1)/) } // 线程安全的方法,增加了互斥锁 func modifySafe(i int) { muMap.Lock() data[i] = i muMap.Unlock() wgMap.Done() }
上面的代码中 var data map[int]int 是一个key和value都是int类型的map,启动的协程并发执行时,也只是非常简单的对 data[i]=i 这样的一个赋值操作。
主程序发起1w个并发,不断对map中不同的key进行赋值操作。
在不安全的情况下,我们直接就看到一个panic异常信息,程序是无法正常执行完成的,如下:
fatal error: concurrent map writes goroutine [running]: runtime.throw(0x4d6e, 0x) C:/Go/src/runtime/panic.go: +0x9c fp=0xcbf sp=0xcbf pc=0xac runtime.mapassign_fast(0x4ba4c0, 0xce, 0xc, 0x0) C:/Go/src/runtime/hashmap_fast.go: +0x3d9 fp=0xcbfa8 sp=0xcbf pc=0xbed9 main.modifyNotSafe(0xc) mainMap.go: +0x4a fp=0xcbfd8 sp=0xcbfa8 pc=0x4a1f1a runtime.goexit() C:/Go/src/runtime/asm_amd.s: +0x1 fp=0xcbfe0 sp=0xcbfd8 pc=0xcc1 created by main.main mainMap.go: +0x
对比之前《 Go实例讲解,并发编程-slice并发读写的线程安全性问题》,slice的数据结构在不安全的并发执行中是不会报错的,只是数据可能会出现丢失。
而这里的map的数据结构,是直接报错,所以在使用中就必须认真对待,否则整个程序是无法继续执行的。
所以也看出来,Go在对待线程安全性问题方面,对slice还是更加宽容的,对map则更加严格,这也是在并发编程时对我们提出了基本的要求。
将上面的代码稍微做些修改,对 data[i]=i 的前后增加上 muMap.Lock() 和 muMap.Unlock() ,也就保证了多线程并行的情况下,遇到冲突时有互斥锁的保证,避免出现线程安全性问题。
关于为什么会出现线程安全性问题,这里就不再详细讲解了,大家可以参考之前的两篇文章《 Go实例讲解,并发编程-slice并发读写的线程安全性问题》和《 Go实例讲解,并发编程-数字递增的线程安全性问题》。
这里,我们再来探讨一个问题,如何保证map的线程安全性?
上面我们是通过 muMap 这个互斥锁来保证的。
而Go语言有一个概念:“不要通过共享内存来进行通信,而应该通过通信来共享内存”,也就是利用channel来保证线程安全性。
那么,这又要怎么来做呢?下面是实例代码:
/** * 并发编程,map的线程安全性问题,使用channel的方式 */ package main import ( "time" "fmt" ) var dataCh map[int]int = make(map[int]int) var chMap chan int = make(chan int) func main() { // 并发启动的协程数量 max := time1 := time.Now().UnixNano() for i := 0; i < max; i++ { go modifyByChan(i) } // 处理channel的服务 chanServ(max) time2 := time.Now().UnixNano() fmt.Printf("data len=%d, time=%d", len(dataCh), (time2-time1)/) } func modifyByChan(i int) { chMap <- i } // 专门处理chMap的服务程序 func chanServ(max int) { for { i := <- chMap dataCh[i] = i if len(dataCh) == max { return } } }
数据填充的方式我们还是用1w个协程来做,只不过使用了chMap这个channel来做队列。
然后在 chanServ 函数中启动一个服务,专门来消费chMap这个队列,然后把数据给map赋值 dataCh[i]=i 。
从上面简单的对比中,我们还看不出太多的区别,我们还是可以得出下面一些
1 通过channel的方式,其实就是通过队列把并发执行的数据读写改成了串行化,以避免线程安全性问题;
2 多个协程交互的时候,可以通过依赖同一个 channel对象来进行数据的读写和传递,而不需要共享变量,可以参考之前的文章《 Go实例讲解,利用channel实现协程的互动-会聊天的Tom&Jerry》;
我们再来对比一下程序的执行效率。
使用互斥锁的方式,执行返回数据如下:
data len=, time=4
使用channel的方式,执行返回数据如下:
data len=, time=
可以看出,这种很简单的针对map并发读写的场景,通过互斥锁的方式比channel的方式要快很多,毕竟channel的方式增加了channel的读写操作,而且channel的串行化处理,效率上也会低一些。
所以,根据具体的情况,我们可以考虑优先用什么方式来实现。
优先使用互斥锁的场景:
1 复杂且频繁的数据读写操作,如:缓存数据;
2 应用中全局的共享数据,如:全局变量;
优先使用channel的场景:
1 协程之间局部传递共享数据,如:订阅发布模式;
2 统一的数据处理服务,如:库存更新+订单处理;
至此,我们已经通过3个Go实例讲解,知道在并发读写的情况下,如何搞定线程安全性问题,简单的数据结构就是int类型的安全读写,复杂的数据结构分别详细讲解了slice和map。在这次map的讲解中,还对比了互斥锁和channel的方式,希望大家能够对并发编程有更深入的理解。
Go 语言入门 2-集合(map)的特性及实现原理
在 Go 语言的世界里,map 是一种独特的数据结构,它以键值对的形式存储数据,以其高效性和独特的哈希管理机制著称。Go 语言的 map 实现由 hmap 结构管理哈希桶数组,而桶的内部结构由 bmap 定义,保证了键的唯一性并提供了近乎瞬时的 O(1) 查找性能。
map 的创建方式有两种:一是通过字面量初始化,二是通过 make 函数,这为灵活性提供了保障。其基本操作包括:通过计算键的哈希值获取索引,对桶进行查找、添加、更新或删除元素。在处理哈希冲突时,Go 采用了一种名为拉链法的策略,当桶满时,会创建新的溢出桶,并通过 next 指针将它们串联起来。然而,随着元素的增长,查询效率会逐渐降低,因此,Go 通过负载因子这一指标来监控冲突,当达到预设阈值(例如,Go 6.5 版本的 0.,Redis 1 和 Java 0. 不同)时,会触发 rehash 过程。当溢出桶数量达到 \(2^{ }\) 时,也会自动进行调整。
rehash 是一个细致的分步操作,它逐步地将旧桶的数据迁移到新桶,确保数据的一致性。这个过程结束后,旧的哈希桶会被释放,以保持内存的高效利用。深入理解 map 的这些核心原理,将有助于你在 Go 的开发旅程中游刃有余。
如果你在实际应用中遇到 map 相关的挑战,别犹豫,我们欢迎你在评论区分享你的问题,让我们共同探索和学习。如果你想了解更多关于 Go 语言的实用技巧,别忘了关注我们的公众号「Python 学习爱好者」,那里有丰富的编程资源和成长社区,期待你的加入。
map在golang的底层实现和源码分析
在Golang 1..2版本中,map的底层实现由两个核心结构体——hmap和bmap(此处用桶来描述)——构建。初始化map,如`make(map[k]v, hint)`,会创建一个hmap实例,包含map的所有信息。makemap函数负责创建hmap、计算B值和初始化桶数组。
Golang map的高效得益于其巧妙的设计:首先,key的hash值的后B位作为桶索引;其次,key的hash值的前8位决定桶内结构体的数组索引,包括tophash、key和value;tophash数组还用于存储标志位,当桶内元素为空时,标志位能快速识别。读写删除操作充分利用了这些设计,包括更新、新增和删除key-value对。
删除操作涉及到定位key,移除地址空间,更新桶内tophash的标志位。而写操作,虽然mapassign函数返回value地址但不直接写值,实际由编译器生成的汇编指令提高效率。扩容和迁移机制如sameSizeGrow和biggerSizeGrow,针对桶利用率低或桶数组满的情况,通过调整桶结构和数组长度,优化查找效率。
evacuate函数负责迁移数据到新的桶区域,并清理旧空间。最后,虽然本文未详述,但订阅"后端云"公众号可获取更多关于Golang map底层实现的深入内容。