偶然看见这么篇文章:一道并发和锁的golang面试题。
虽然年代久远,但也稍有兴趣。git
正好最近也看到了 sync.Map,因此想试试能不能用 sync.Map 去实现上述的功能。github
我还在 gayhub上找到了其余人用 sync.Mutex 的实现方式,【点击这里】。golang
需求是这样的:web
在一个高并发的web服务器中,要限制IP的频繁访问。现模拟100个IP同时并发访问服务器,每一个IP要重复访问1000次。每一个IP三分钟以内只能访问一次。修改如下代码完成该过程,要求能成功输出 success: 100。
而且给出了原始代码:面试
package main import ( "fmt" "time" ) type Ban struct { visitIPs map[string]time.Time } func NewBan() *Ban { return &Ban{visitIPs: make(map[string]time.Time)} } func (o *Ban) visit(ip string) bool { if _, ok := o.visitIPs[ip]; ok { return true } o.visitIPs[ip] = time.Now() return false } func main() { success := 0 ban := NewBan() for i := 0; i < 1000; i++ { for j := 0; j < 100; j++ { go func() { ip := fmt.Sprintf("192.168.1.%d", j) if !ban.visit(ip) { success++ } }() } } fmt.Println("success: ", success) }
哦吼,看到源代码我想说,我能只留个package main
其余都从新写吗?(捂脸)数据库
聪明的你已经发现,这个问题关键就是想让你给 Ban 加一个读写锁罢了。
并且条件中的三分钟根本无伤大雅,由于这程序压根就活不到那天。安全
其实,原始的思路并无发生改变,仍是用一个 BanList 去盛放哪些暂时没法访问的用户 id。
而后每次访问的时候判断一下这个用户是否在这个 List 中。bash
好,那咱们如今须要一个结构体,由于咱们会并发读取 map,因此咱们直接使用 sync.Map:服务器
type Ban struct { M sync.Map }
若是你点进 sync.Map 你会发现他真正存储数据的是一个atomic.Value
。
一个具备原子特性的 interface{}。并发
同时Ban这个结构提还会有一个 IsIn
的方法用来判断用户 id 是否在Map中。
func (b *Ban) IsIn(user string) bool { fmt.Printf("%s 进来了\n", user) // Load 方法返回两个值,一个是若是能拿到的 key 的 value // 还有一个是否可以拿到这个值的 bool 结果 v, ok := b.M.Load(user) // sync.Map.Load 去查询对应 key 的值 if !ok { // 若是没有,说明能够访问 fmt.Printf("名单里没有 %s,能够访问\n", user) // 将用户名存入到 Ban List 中 b.M.Store(ip, time.Now()) return false } // 若是有,则判断用户的时间距离如今是否已经超过了 180 秒,也就是3分钟 if time.Now().Second() - v.(time.Time).Second() > 180 { // 超过则能够继续访问 fmt.Printf("时间为:%d-%d\n", v.(time.Time).Second(), time.Now().Second()) // 同时从新存入时间 b.M.Store(ip, time.Now()) return false } // 不然不能访问 fmt.Printf("名单里有 %s,拒绝访问\n", user) return true }
下面看看测试的函数:
func main() { var success int64 = 0 ban := new(Ban) wg := sync.WaitGroup{} // 保证程序运行完成 for i := 0; i < 2; i++ { // 咱们大循环两次,每一个user 连续访问两次 for j := 0; j < 10; j++ { // 人数预先设定为 10 我的 wg.Add(1) go func(c int) { defer wg.Done() ip := fmt.Sprintf("%d", c) if !ban.IsIn(ip) { // 原子操做增长计数器,用来统计咱们人数的 atomic.AddInt64(&success, 1) } }(j) } } wg.Wait() fmt.Println("这次访问量:", success) }
其实测试的函数并无作大的改动,只不过,由于咱们是并发去运行的,须要增长一个 sync.WaitGroup() 保证程序完整运行完毕后才退出。
我特意把运行数值调小一点,以方便测试。
把1000
次请求,改成2
次。100
人改成10
人。
因此整个代码应该是这样的:
package main import ( "fmt" "sync" "sync/atomic" "time" ) type Ban struct { M sync.Map } func (b *Ban) IsIn(user string) bool { ... } func main() { ... }
运行一下...
诶,彷佛不太对哦,发现会出现 10~15 次不等的访问量结果。为何呢?
寻思着,其实由于并发致使的,看到这里了吗?
func (b *Ban) IsIn(user string) bool { ... v, ok := b.M.Load(user) if !ok { fmt.Printf("名单里没有 %s,能够访问\n", user) b.M.Store(ip, time.Now()) return false } ... }
并发发起的 sync.Map.Load
其实并无与 sync.Map.Store
链接起来造成原子操做。
因此若是有3个 user 同时进来,程序同时查询,三个返回结果都会是 false(不在名单里)。
因此也就增长了访问的数量。
其实 sync.Map 也已经考虑到了这种状况,因此他会有一个 LoadOrStore
的原子方法--
若是 Load 不出,就直接 Store,若是 Load 出来,那啥也不作。
因此咱们小改一下 IsIn 的代码:
func (b *Ban) IsIn(user string) bool { ... v, ok := b.M.LoadOrStore(user, time.Now()) if !ok { fmt.Printf("名单里没有 %s,能够访问\n", user) // 删除b.M.Store(ip, time.Now()) return false } ... }
而后咱们再运行一下,运行几回。
发觉不会再出现 这次访问量大于 10 的状况了。
到此为止,这个场景下的代码实现咱们算是成功了。
可是真正限制用户访问的场景需求可不能这么玩,通常仍是配合内存数据库去实现。
那么,若是你只想了解 sync.Map 的应用,就到这里为止了。
然而好奇心驱使我看看 sync.Map 的实现,咱们继续吧。
若是硬是要并发读写一个 go map 会怎么样?
试一下:
先来个主角 A
type A map[string]int
咱们定义成了本身一个类型 A,他骨子里仍是 map。
type A map[string]int func main() { // 初始化一个 A m := make(A) m.SetMap("one", 1) m.SetMap("two", 3) // 读取 one go m.ReadMap("one") // 设置 two 值为 2 go m.SetMap("two", 2) time.Sleep(1*time.Second) } // A 有个读取某个 Key 的方法 func (a *A)ReadMap(key string) { fmt.Printf("Get Key %s: %d",key, a[key]) } // A 有个设置某个 Key 的方法 func (a *A)SetMap(key string, value int) { a[key] = value fmt.Printf("Set Key %s: %d",key, a[key]) // 同协程的读写不会出问题 }
诶,看上去不错,咱们给 map A 类型定义了 get, set 方法,若是 golang 不容许并发读写 map 的话,应该会报错吧,咱们跑一下。
> Get Key one: 1 > Set Key two: 2
喵喵喵???
为何正常输出了?
说好的并发读写报错呢?
好吧,其实缘由是上面的 map 读写,虽然咱们设置了协程,可是对于计算机来讲仍是有时间差的。只要一个微小的前后,就不会形成 map 数据的读写异常,因此咱们改一下。
func main() { m := make(A) m["one"] = 1 m["two"] = 3 go func() { for { m.ReadMap("one") } }() go func(){ for { m.SetMap("two", 2) } }() time.Sleep(1*time.Second) }
为了让读写可以尽量碰撞,咱们增长了循环。
如今咱们能够看到了:
> fatal error: concurrent map read and map write
*这里之因此为有 panic 是由于在 map 实现中进行了并发读写的检查
。
其实上面的例子和 go 对 sync.Mutex 锁的入门教程很像。
咱们证明了 map 并发读写的问题,如今咱们尝试来解决。
既然是读写形成的冲突,那咱们首先考虑的即是加锁。
咱们给读取一个锁,写入一个锁。那么咱们如今须要讲单纯的 A map 转换成一个带有锁的结构体:
type A struct { Value map[string]int mu sync.Mutex }
Value 成了真正存放咱们值的地方。
咱们还要修改下 ReadMap
和 SetMap
两个方法。
func (a *A)ReadMap(key string) { a.mu.Lock() fmt.Printf("Get Key %s: %d",key, a.Value[key]) a.mu.Unlock() } func (a *A)SetMap(key string, value int) { a.mu.Lock() a.Value[key] = value a.mu.Unlock() fmt.Printf("Set Key %s: %d",key, a.Value[key]) }
注意,这里两个方法中,哪个少了 Lock 和 Unlock 都不行。
咱们再跑一下代码,发现能够了,不会报错。
咱们算是用最简单的方法解决了眼前的问题,可是这样真的没问题吗?
细心的你会发现,读写咱们都加了锁,并且没有任何特殊条件限制,因此当咱们要屡次读取 map 中的数据的时候,他喵的都会阻塞!就算我压根不想改 map 中的 value...
尽管如今感受不出来慢,但这对密集读取来讲是一个性能坑。
为了不没必要要的锁,咱们彷佛还要让代码“聪明些”。
没错,读写分离就是一个十分适用的设计思路。
咱们准备一个 Read map,一个 Write map。
但这里的读写分离和咱们平时说的不太同样(记住咱们的场景永远是并发读写),咱们不能实时或者定时让写入的 map 去同步(增删改)到读取的 map 中,
由于...这样和上面的 map 操做没有任何区别,由于读取 map 没有锁,仍是会发生并发冲突。
咱们要解决的是,不“显式”增删改 map 中的 key 对应的 value。
咱们把问题再分类一下:
第一个问题:
咱们把 key 的 value 变成指针怎么样?
相同的 key 指向同一个指针地址,指针地址又指向真正值的地址。
key -> &地址 -> &真正的值地址
Read 和 Write map 的值同时指向一个&地址
,不论谁改,你们都会变。
当咱们须要修改已有的 key 对应的 value 时,咱们修改的是&真正的值地址
的值,并不会修改 key 对应的&地址
或值。
同时,经过atomic
包,咱们可以作到对指针修改的原子性。
太棒了,修改已有的 key 问题解决。
第二个问题:
由于并不存在这个 key,因此咱们必定会增长新 key,
既然咱们有了 Read map & Write map,那咱们能够利用起来呀,
咱们在 Write map 中加锁并增长这个 key 和对应的值,这样不影响 Read map 的读取。
不过,Read map 咱们终究是要更新的,因此咱们加一个计数器 misses
,到了必定条件,咱们把 Write map 安全地同步到 Read map 中,而且清空 Write map。
Read map 能够看作是 Write map 的一个只读拷贝,不容许自行增长新 key,只能读或者改。
上面的思想其实和 sync.Map 的实现离得很近了。
只不过,sync.Map 把咱们的 Write map
叫作了 dirty
,把 Write map 同步
到 Read map 叫作了 promote(升级)
。
又增长了一些结构体封装,和状态标识。
其实 google 一下你就会发现不少分析 sync.Map 源码的文章,都写得很好。我这里也不赘述了,可是我想用个人理解去归纳下
sync.Map 中的方法思路。
结合 sync.Map 源码食用味道更佳。
未命中
(misses + 1),顺便看看咱们的 dirty 是否是能够升级成 Read 了*这里2中之因此再上锁,是为了double-checking,防止在极小的时间差内产生脏读(dirty忽然升级 Read)。
emmmm......
大体就是这样的思路,我这里再推荐一些正统的源码分析和基准测试,相信看完之后会对 sync.Map 更加清晰。
另外,若是你注意到 Map.Store 中第6步的所有复制
的话,你就会有预感,sync.Map 的使用场景其实不太适合高并发写的逻辑。
的确,官方说明也明确指出了 sync.Map 适用的场景:
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use // by multiple goroutines without additional locking or coordination. ... // The Map type is specialized. Most code should use a plain Go map instead, // with separate locking or coordination, for better type safety and to make it // easier to maintain other invariants along with the map content. // // The Map type is optimized for two common use cases: (1) when the entry for a given // key is only ever written once but read many times, as in caches that only grow, // or (2) when multiple goroutines read, write, and overwrite entries for disjoint // sets of keys. In these two cases, use of a Map may significantly reduce lock // contention compared to a Go map paired with a separate Mutex or RWMutex.
sync.Map 只是帮助优化了两个使用场景:
其实 sync.Map 仍是在性能和安全之间,找了一个本身以为合适的平衡点,就如同咱们开头的案例同样,其实 sync.Map 也并不适用。
另外,这里有一个 【sync.Map 的进阶版本】。
其实在好久之前翻看 sync Map 源码的时候,我不经会抛出疑问,若是可以用 atomic 来解决并发安全问题,为何还要 mutex 呢?
并且,在进行 map.Store 的过程当中,仍是会直接修改 read 的 key 所对应的值(而且无锁状态),这和普通修改一个 key 的值有什么区别呢?
若是 atomic 能够保证原子性,那和 mutex 有什么区别呢?
在翻查了一些资料后,我知道了:
Mutexes are slow, due to the setup and teardown, and due to the fact that they block other goroutines for the duration of the lock.Atomic operations are fast because they use an atomic CPU instruction, rather than relying on external locks to.
互斥锁实际上是经过阻塞其余协程起到了原子操做的功能,可是 atomic 是经过控制更底层的 CPU 指令,来达到值操做的原子性的。
因此 atomic 和 mutex 并非一个层面的东西,并且在专职点上也不尽相同,mutex 不少地方也是经过 atomic 去实现的。
而 sync Map 很巧妙地将两个结合来实现并发安全。
其中第一点是不少源码解读中经常一笔带过的,然而萌新我以为反而是至关重要的技巧(捂脸)。
一直没有明白,为何从 dirty map 升级成 read map 的条件是misses 次数大于等于 len(m.dirty)
?
咱们能够看到下面两篇关于不加锁的叙述: