写点什么

[Go WebSocket] 多房间的聊天室(六)为什么要加锁?不加锁行不行啊?

作者:HullQin
  • 2022 年 9 月 15 日
    广东
  • 本文字数:3212 字

    阅读完需:约 11 分钟

我是 HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者 HullQin 授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加 Game Jam 2022。喜欢可以关注我 HullQin 噢~我有空了会分享做游戏的相关技术。

背景

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


《单房间的聊天室》,介绍了如何实现一个单房间的聊天室。


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


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


《多房间的聊天室(三)自动清理无人房间》,介绍了如何清理无人的房间,避免内存无限增长的问题。


《多房间的聊天室(四)黑天鹅事件》,介绍了如何避免并发导致的资源竞争的问题,是通过悲观锁解决的。


《多房间的聊天室(五)用多个小锁代替大锁,提高效率》,介绍了通过把一个全局大锁拆分成多个小锁,提高了并发效率。


温馨提示:阅读本文前,最好阅读下上面的文章。因为这篇文章更复杂,如果你不弄懂上面几篇,这篇可能跟不上节奏噢。


本文回答以下问题:


为什么一定要加锁?加锁后,你的代码逻辑是对的吗?


换个描述方法,本文是论证了给代码逻辑加锁的 必要性充分性

思路

论证必要性

我们只要把加锁相关代码删掉,模拟真实环境发高并发的请求,测试是否有 bug 出现。一旦出现 bug,就认为加锁是必要的。

论证充分性

我们把锁的代码加上,模拟真实环境发高并发的请求,测试 bug 是否修复。并且认为修改代码,增加延时,扩大 bug 出现可能性。如果最终没有任何 bug,表明现在服务器代码逻辑是正确的。

分析验证的目标

如果把代码里没有锁,会有什么问题?


我们把锁相关代码注释掉,分析下代码:



建立连接时,我们根据 roomId,从 house 中获取了 room。


但是 goroutine 随时有可能主动让出执行权,去执行其它 goroutine。而且我们知道,这个r.HandleFunc()里的函数,是每有一个客户端请求连接时,都是一个新的 goroutine。


如果同时有多个客户端请求连接同一个房间,他们都刚好在room, ok := house[roomId]后,让出了执行权,那么他们都发现该房间都不存在,各自建立了新房间,导致他们虽然房间号相同,但是房间不同,无法相互通信。


怎么测试这个场景呢?


我们同时在客户端启动多个 WebSocket 连同一个房间即可(假设启动 10 个连接),然后等所有客户端连接完毕后,各自发一个消息,看看每个客户端收到了多少消息。


在同一个房间里,有 10 个人进来了,每人发了 1 个广播消息,这个消息应该会广播给 10 个人。有 10 个人发了消息,所以预期每个人会收到 10 个消息。一共就是 100 条消息。


但是如果 Bug 发生,应该是少于 100 条消息的,我们验证一下!

开发测试脚本

因为我们提供的 WebSocket 服务的客户端是在浏览器中,所以,为了模拟生产环境,我们的测试脚本也用htmljs开发。


因为是高并发,所以一个页面可以开启多个 WebSocket 连接,通过输入参数,可以调整房间数目、每个房间的客户端的数目。


交互如下:



代码如下:


<body><div>  <label>    Number of rooms: <input type="number" id="rooms" value="1">  </label></div><div>  <label>    Number of clients of per room: <input type="number" id="clients" value="11">  </label></div><div>  <button id="start">Start</button></div><script>  const startBtn = document.getElementById('start');  const roomsInput = document.getElementById('rooms');  const clientsInput = document.getElementById('clients');  const data = {    succ: 0,    fail: 0,  };  startBtn.addEventListener('click', () => {    const rooms = parseInt(roomsInput.value);    const clients = parseInt(clientsInput.value);    if (!rooms || !clients || rooms < 0 || clients < 0) return;    for (let i = 0; i < rooms; i++) {      for (let j = 0; j < clients; j++) {        const ws = new WebSocket(`ws://127.0.0.1:8080/ws/room-${i}`);        let succ = false;        let received = 0;        const delay = [];        ws.onopen = () => {          succ = true;          data.succ += 1;          console.log(data);          setTimeout(() => {            ws.send(new Date().getTime().toString());          }, 1000);          setTimeout(() => {            console.log({ room: i, client: j, received, delay });            ws.close(1000);          }, 2000);        };        ws.onerror = () => {          if (succ) data.succ -= 1;          data.fail += 1;        };        ws.onmessage = (e) => {          e.data.split('\n').forEach((sendTime) => {            delay.push(new Date().getTime() - Number(sendTime));            received += 1;          });        };      }    }  });</script></body>
复制代码

测试注意事项

注意控制变量

测试一共分 2 个步骤:


  1. 注释所有锁相关代码,执行测试,验证 Bug 会出现

  2. 取消注释所有锁相关代码,执行测试,验证 Bug 不再出现


所以记得在 Go 代码里搜索一下LockUnlock关键词,3 个文件都有锁相关的代码,共计十多行代码。


此外,其它变量就不必修改了(例如客户端数目、房间数目)。这就是大家都知道的控制变量法

检查是否有跨域问题

如果你客户端域名和服务的域名不同,我们的gorilla/websocket是默认不支持跨域的。


如果你是直接修改了chat-multi-rooms文件夹里面的home.html文件,那么不会有跨域问题,你启动服务后访问localhost:8080即可。


如果你是直接打开了 html 文件,域名跟服务器不同,就需要修改服务器代码,支持一下跨域。在client.go文件的第 35 行附近,修改为:


var upgrader = websocket.Upgrader{   ReadBufferSize:  1024,   WriteBufferSize: 1024,   CheckOrigin: func(r *http.Request) bool {      return true   },}
复制代码


这样针对任意来源 origin,都不会报跨域问题了。

扩大 Bug 发生概率

可以在建立房间前,使用函数runtime.Gosched()主动让出 goroutine 执行权、并且睡眠一点点时间,扩大 Bug 发生概率。


选择测试设备

务必选择多种浏览器、多种设备来测试,因为我用 Google Chrome 测试时,就发现个坑。


Google Chrome 有个特点:如果你同时建立多个 WebSocket 连接,之后一个一个建立。等前一个建立连接成功,后一个才有序的开始建立连接。因此这样就导致服务器不会出现 Bug(我甚至一度怀疑我后端代码不需要加锁,怀疑是 Go 内部的代码自动加锁了,但是看了半天 Go 源码,也没发现底层有自动加锁逻辑)。


后来我用 safari 浏览器测试,就很容易复现 Bug 了。

测试结果

删掉锁相关代码

在 MacOS 的 safari 浏览器下,设置房间数=1,客户端连接数=11。


浏览器输出如下:



其中 received 字段就表示各个客户端收到了多少条消息,delay 是收到每条消息的毫秒延迟(用收到时间-发送时间)。


Go 服务输出如下:



可以看到输出了 6 个create,意思是虽然 11 个客户端连接同一个房间,但是却创建了 6 次房间。必然导致每个客户端收到的消息小于 11 条。

加上锁相关代码

在 MacOS 的 safari 浏览器下,设置房间数=1,客户端连接数=11。


浏览器输出如下:



Go 服务输出如下:



可以看到,加锁后,非常完美,只有第一个连接建立时发生了create房间,其它都正常加入了。而且每个客户端都收到了 11 条请求,符合预期。

写在最后

我是 HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,发送加微信,交个朋友),转发本文前需获得作者 HullQin 授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费没广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加 Game Jam 2022。喜欢可以关注我 HullQin 噢~我有空了会分享做游戏的相关技术。

发布于: 刚刚阅读数: 4
用户头像

HullQin

关注

公众号【线下聚会游戏】 2020.10.07 加入

game.hullqin.cn 我做了一些联机桌游网页:支持2-10人联机的UNO、2-4人联机的斗地主、2人联机的五子棋。无需下载,点开即玩!叫上朋友,即刻开局!不看广告,不做任务,享受「纯粹」的游戏!

评论

发布
暂无评论
[Go WebSocket] 多房间的聊天室(六)为什么要加锁?不加锁行不行啊?_Go_HullQin_InfoQ写作社区