[Go WebSocket] 多房间的聊天室(七)删除房间时,顺便清除房间锁

[Go WebSocket] 多房间的聊天室(七)删除房间时,顺便清除房间锁

我报名参与金石计划1期挑战——分割10万奖池,这是我的第2篇文章,点击检查活动详情。

大家好,我是公众号「线下聚会游戏」作者,开发了《联机桌游合集》,是个网页,能够很便利的跟朋友联机玩斗地主、五子棋等游戏。其中的核心技能便是WebSocket,我会共享怎么用Go完成WebSocket服务,文章写在专栏《Go WebSocket》里,重视专栏跟我一同学习吧!

布景

在专栏《Go WebSocket》里,有一些前置文章:

《单房间的聊天室》,介绍了怎么完成一个单房间的聊天室。

《多房间的聊天室(一)考虑篇》,介绍了完成一个多房间的聊天室的思路。

《多房间的聊天室(二)代码完成》,介绍了完成一个多房间的聊天室的代码。

《多房间的聊天室(三)自动整理无人房间》,介绍了怎么整理无人的房间,防止内存无限增长的问题。

《多房间的聊天室(四)黑天鹅事件》,介绍了怎么防止并发导致的资源竞赛的问题,是经过失望锁处理的。

《多房间的聊天室(五)用多个小锁替代大锁,进步功率》,介绍了经过把一个大局大锁拆分红多个小锁,进步了并发功率。

《多房间的聊天室(六)为什么要加锁?不加锁行不行啊?》,介绍了加锁的必要性和正确性。

可是到目前为止,咱们的多房间聊天室仍是不行完美,存在2个问题:

  1. roomMutexes是一个大局map,当房间被整理时,这个map里依然保存着key为roomId的sync.Mutex。跟着时间延长,这个map会越来越大……而且大多数都用不到了。假如有人想歹意进犯你的体系,只需要接二连三的拜访不同的房间号,那么你体系内存会被打爆的。总归这个体系不行耐久、也比较脆弱。
  2. house变量是一个大局map,而Go中map是不支持并发写的。对某个map履行设置key或删去key,都不是原子的,当某个goroutine设置/删去key做了一半,CPU被切到另一个goroutine,去履行设置/删去同一map的某个key,就会报错fatal error: concurrent map writes。因而假如map存在大量并发写时,会导致犯错概率进步。这种状况下,要用sync.map。在本文章的场景下,写入map有如下场景:用户进了某个新房间(没人的房间)、用户脱离了某个只剩一人的房间。并发量大的场景下,是很有可能呈现同时有n个用户进入n个不同的新房间的。所以结论是,house应该运用sync.map,不能用map。(可是roomMutexes这个map没有问题,因为不触及并发写,在写入前是加了大局锁的)

跟着走

本文代码起点:github.com/HullQin/go-…

本文是基于前六篇文章的,所以至上篇文章,代码现已更新到这个commit了,你能够Pull下来跟着一同考虑、学习、修正。

先处理问题2: 替换house为sync.map

留意sync.map是没有类型的,所以读取后需要强制类型转换。

重视这个commit: github.com/HullQin/go-…

[Go WebSocket] 多房间的聊天室(七)删除房间时,顺便清除房间锁

再处理问题1: 整理房间时,也整理该房间的锁

考虑:这样改能够吗?

回忆一下整理房间的逻辑:

[Go WebSocket] 多房间的聊天室(七)删除房间时,顺便清除房间锁

抛出一个问题:我能够直接修正下面这段逻辑吗?

原逻辑:

if len(h.clients) == 0 {
   house.Delete(h.roomId)
   roomMutexes[h.roomId].Unlock()
   return
}

为了整理房间锁,改为这样,能够吗?

if len(h.clients) == 0 {
   house.Delete(h.roomId)
   roomMutexes[h.roomId].Unlock()
   delete(roomMutexes, h.roomId)
   return
}

你考虑下。结合进入房间的逻辑:

[Go WebSocket] 多房间的聊天室(七)删除房间时,顺便清除房间锁

剖析

答案是不能够。

进入房间时,需要拜访roomMutexes,咱们设置了大局锁。整理房间时,咱们设置的锁仅仅是房间维度的锁。二者没有抵触,是有机会并发的。一旦并发,就可能导致各种问题,例如:

  1. 一个goroutine准备整理房间时,即将履行delete(roomMutexes, h.roomId)时,恰好调度另一个goroutine,履行roomMutex, ok := roomMutexes[roomId],运用了即将被删掉的锁。然后这个锁从roomMutexes这个map里删掉了。随后又有一个进入该房间的人,履行roomMutexes[roomId] = new(sync.Mutex)重生成了锁。那么同一房间的2个人进入同一个房间,可是运用的是2把不同的锁。这会导致其它不可思议的问题。
  2. roomMutexes是普通的map,不是sync.map,所以并发写、删会有抵触。可是这个问题不致命,因为处理该问题,只要设置roomMutexes为sync.map即可。可是即便这样,问题1也无法防止。

所以,教训便是:咱们删去roomMutexes中的房间锁时,必需要先设置大局锁,再进行删去。

这样确保了「进入房间时取得锁」和「脱离房间时删去锁」,进程都是原子的,就没并发抵触。

深度考虑:这样能够吗?

假如简略的在delete这个房间锁前,获取一下这个大局锁mutexForRoomMutexes能够吗?

select {
case client := <-h.unregister:
   roomMutex := roomMutexes[h.roomId]
   roomMutex.Lock()
   if _, ok := h.clients[client]; ok {
      delete(h.clients, client)
      close(client.send)
      if len(h.clients) == 0 {
         house.Delete(h.roomId)
         mutexForRoomMutexes.Lock()
         delete(roomMutexes, h.roomId)
         roomMutex.Unlock()
         mutexForRoomMutexes.Unlock()
         return
      }
   }
   roomMutex.Unlock()

你考虑一下。记得看看进入房间的逻辑。

深度剖析

答案是不能够。

这是一个典型的死锁事例。

凡是你在代码里有2把锁(不论大锁仍是小锁),咱们称为锁A和锁B。假如你有2个goroutine别离有这种逻辑:

goroutine1 伪代码:
取得锁A
取得锁B
开释锁B
开释锁A
goroutine2 伪代码:
取得锁B
取得锁A
开释锁A
开释锁B

那么你大概率会遇到死锁问题。2个goroutine都卡住了,程序没有响应。

在上面这段处理计划的代码逻辑里,大锁和房间锁,别离便是锁A和锁B。进入房间的逻辑,相当于goroutine1,删去房间的逻辑,相当于goroutine2。

处理这种形式死锁的典型计划:一直按照顺序取得锁A和锁B。

假如一切goroutine都是这样写:

goroutine 伪代码:
取得锁A
取得锁B
开释锁B或A
开释锁A或B

就防止了死锁问题。咱们把大锁当作锁A,房间锁当作锁B。就能够写出下面的代码:

[Go WebSocket] 多房间的聊天室(七)删除房间时,顺便清除房间锁

你或许会猎奇,为什么49行要用roomMutex.TryLock()

TryLock:测验获取Mutex,假如当时Mutex没有被Lock,就相当于Lock()并返回true,不然,返回false,并继续履行下面的逻辑。

首要是因为咱们是先roomMutex.Unlock()mutexForRoomMutexes.Lock()。在这两行之间,goroutine没有取得任何锁,有可能此刻其它人进入了该房间,抢先取得了mutexForRoomMutexes大局锁,而且加入了房间。有2种状况:

  1. 进入房间的人,开释mutexForRoomMutexes大局锁后,还没开释roomMutex房间锁(因为进入房间逻辑是先开释前者,后开释后者)。此刻roomMutex.TryLock()获取房间锁失败,不再履行铲除房间锁逻辑。这把锁能够被新创建的房间复用。
  2. 进入房间的人,开释了mutexForRoomMutexes大局锁,而且也开释了roomMutex房间锁(即serveWs逻辑也履行完了),此刻该人确实进入了房间,h.clients不再是0了。这时roomMutex.TryLock()确实能取得成功锁,咱们就判断一下h.clients长度,假如为0,才删去这个房间锁,非0就什么都不做。最终开释这个房间锁。

源码

库房地址:github.com/HullQin/go-…

重视这2个commit:

  • replace house map with sync.map
  • delete room mutex when deleting room

写在最终

我是HullQin,独立开发了《联机桌游合集》,是个网页,能够很便利的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参与Game Jam 2022。喜爱能够重视我噢~我有空了会共享做游戏的相关技能,会在这2个专栏里共享:《教你做小游戏》、《极致用户体会》。