1.Go语言学习(2)--map的阅o源底层原理
2.Go实例讲解,并发编程-map并发读写的读源读技线程安全性问题
3.Go同步原语之sync/Map
4.map的基本使用-go篇
5.Go语言sync.Map实现原理
6.golang map 源码解读(8问)
Go语言学习(2)--map的底层原理
Golang的Map底层是通过HashTable实现的,创建map时实际返回的码g码阅是runtime/map.go中hmap对象的指针。hmap中buckets指向的阅o源是bucket数组的指针,bucket数组大小由B决定,读源读技通常为2^B个。码g码阅11101010源码单个bucket结构体内部不直接定义keys、阅o源values和overflow,读源读技而是码g码阅通过指针运算访问。
在查找、阅o源插入和删除过程中,读源读技通过哈希函数将键转换为哈希值,码g码阅然后使用哈希值对bucket进行定位。阅o源查找时直接访问哈希表中对应的读源读技bucket,插入和删除操作涉及更新bucket中的码g码阅键值对。
Map的扩容机制基于负载因子,负载因子用于衡量冲突情况,定义为bucket数量与键值对数量的比值。当负载因子大于6.5,或者overflow数量超过时,Map会触发扩容。扩容时,新bucket长度为原bucket长度的2倍,旧bucket数据搬迁到新bucket。为了减少一次性搬迁带来的延迟,Go采用逐步搬迁策略,每次访问map时触发搬迁,每次搬迁2个键值对。
扩容后,新bucket存储新插入的键值对,老bucket中的键值对逐步搬迁到新bucket。搬迁完成后,删除老bucket。搬迁过程中,老bucket中的键值对将位于新bucket的前部,新插入的键值对位于新bucket的后部。
等量扩容是重新组织bucket,提升bucket的使用率,而不是简单地增加容量。在某些极端场景下,如果键值对集中在少数bucket,获取账号盒子源码可能导致overflow的bucket数量增多,但负载因子不高,无法执行增量搬迁。这时进行一次等量扩容,可以减少overflow的bucket数量,优化访问效率。
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的easypr源码解析svm场景:
1 协程之间局部传递共享数据,如:订阅发布模式;
2 统一的数据处理服务,如:库存更新+订单处理;
至此,我们已经通过3个Go实例讲解,知道在并发读写的情况下,如何搞定线程安全性问题,简单的数据结构就是int类型的安全读写,复杂的数据结构分别详细讲解了slice和map。在这次map的讲解中,还对比了互斥锁和channel的方式,希望大家能够对并发编程有更深入的理解。
Go同步原语之sync/Map
在Go语言中,map的并发使用是不安全的,当多个goroutine同时对一个map进行读写操作时,会发生数据竞争问题。通过使用`go`自带的`-race`检测,可以发现存在数据竞争,即`data race`。解决数据竞争问题,常见的方法是加锁。然而,加锁会带来大量的开销,在高并发场景下,锁的争用会导致系统性能下降。因此,Go语言提供了内置的`sync.Map`,它可以解决并发问题。
`sync.Map`是`sync`包下的一个特殊结构,它对外提供了常用的方法。基本使用示例包括加载、存储和删除数据。`sync.Map`的核心数据结构包括`read`和`dirty`两个map。`sync.Map`的`Load`函数是负责查询和读取map数据的函数,它通过`read`缓存层优先读取数据,提高了读性能。`Store`函数用于更新数据,而`Delete`函数则负责删除Map中的数据。`Range`函数提供了遍历`sync.Map`的方式,保证了在遍历过程中并发读写操作的正确性。
`sync.Map`的实现原理中,`read`好比整个`sync.Map`的仿即刻贷源码“高速缓存”,当goroutine从`sync.Map`中读取数据时,会首先查看`read`缓存层是否有需要的数据(key是否命中),如果有(key命中),则通过原子操作将数据读取并返回。这就是`sync.Map`推荐的快路径,也是其读性能极高的原因。
`sync.Map`的`Store`函数用于更新数据,它首先检查`read`缓存层是否有对应key的数据,如果有,则直接更新;如果没有,则需要先删除`read`缓存层的数据,然后通过加锁的方式在`dirty`map中更新数据。`Delete`函数则会从`sync.Map`中删除指定的key及其对应的value。
`sync.Map`的`Range`函数提供了O(N)的方式遍历`sync.Map`,遍历过程中,如果在`dirty`map中发生了新的写入操作,会将`dirty`map中的状态提升为`read`map的状态,确保遍历的原子性。
`orcaman/concurrent-map`是一个适用于反复插入和读取新值的库。它通过分片加锁的方式,降低锁的粒度,从而达到最少的锁等待时间,实现并发安全。
`sync.Map`的设计目的是为了在读多写少的情况下,提供并发安全的同时又不需要锁的开销。然而,在写多读少的情况下,它的性能可能不如常规的`map`。因此,选择合适的并发数据结构取决于具体的应用场景和性能需求。
map的基本使用-go篇
map是编程中用于存储键值对的结构,适用于存储各种数据。
创建map有多种方式,包括使用make()函数并可选地赋初始值。
任何类型都可作为map的键,包括字符串、整数、布尔值、浮点数、字节、结构体和指针,甚至接口类型。
map的值可以是任意类型。
操作map涉及初始化、添加、修改、删除和查询。
添加值前需确保map已初始化,否则会触发panic。
修改不存在的值时,该值会自动添加到map中。
删除键时,如果键不存在,不会引起panic。
查询不存在的键时,返回值的零值。
map查询可返回键值和一个布尔值,表示键是否存在。
查询map的值时,若键不存在,返回类型零值。
初始化嵌套map时,逻辑与普通map相似,只是代码可能较长。
确保在操作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,实现高效、安全的并发管理。
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,不初始化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 语言入门 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 学习爱好者」,那里有丰富的编程资源和成长社区,期待你的加入。
2024-12-22 09:14
2024-12-22 09:05
2024-12-22 08:56
2024-12-22 08:40
2024-12-22 08:21
2024-12-22 07:18