我是HullQin,公众号线下团聚游戏的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者HullQin受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。

背景

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

第一篇文章:《为什么我选用Go重构Python版本的WebSocket服务?》,介绍了我的指标。

第二篇文章:《你的第一个Go WebSocket服务: echo server》,介绍了一下怎么写一个WebSocket server。

第三篇文章:《单房间的聊天室》,介绍了如何实现一个单房间的聊天室。

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

第五篇文章:《多房间的聊天室(二)代码实现》,介绍了实现一个多房间的聊天室的代码。

第六篇文章:《多房间的聊天室(三)主动清理无人房间》,介绍了如何清理无人的房间,防止内存有限增长的问题。

如果你没浏览下面的文章,肯定要先看一下,因为这篇文章更简单,如果你不弄懂下面几篇,这篇可能跟不上节奏噢。

黑天鹅事件

回顾一下咱们的goroutine架构图:

我用连线,表明了goroutine的启动关系:

  • User连贯WebSocket服务器时,会先启动serveWs goroutine
  • serveWs goroutine中,会执行register操作,这一点之前的图中并没画进去。
  • 随后serveWs goroutine启动了Read goroutineWrite goroutine,并完结本人。

此外,咱们晓得,同一个goroutine内是按程序执行的,多个goroutine的执行程序是没保障的,任何一个goroutine都可能执行到某行代码时长期中断,去执行其它goroutine的代码。

咱们剖析一下:

register和unregister竞争

可能性1

有没有这种可能?register执行到一半,在筹备给h.clients减少一个client前,先执行了另一个client的unregister,这时候刚好是无人房间,而后房间被删了,然而执行到一半的register继续执行,给h.clients减少了一个client。导致了异样。

答案是:不可能。为什么?

因为解决register和unregister的hub是一个goroutine,它外部会一直轮询register、unregister、broadcast这几个channel。如果接管到某个,会把它解决完,再去解决下一个。所以针对同一个room,不存在register执行了一半,又去执行unregister的可能。

可能性2

有没有这种可能?serceWs执行到一半,发现当初某个房间存在且用户数=1,刚给register发送了数据,此时hub goroutine还没开始解决这个register的数据。然而另一个(房间内惟一的)客户端刚好同时断开了连贯,给这个房间发送了unregister数据,hub优先解决了unregister的数据,把房间删除了。这时候hub goroutine完结了,之前的register channel也就被敞开了,数据被抛弃了,导致用户进入房间失败。

这里serveWs和hub是2个不同的goroutine,这种状况是可能产生的,只是须要一点点差运气,概率很低。

尽管概率低,然而绝不可疏忽这种极其状况。如果你心愿规模做大,必须至多在逻辑上保障100%的正确率。

毕竟咱们当初写的不是coroutine协程,协程的执行程序是开发者很容易通过asyncawait语法掌控的。goroutine的执行程序并非齐全由开发者掌控,须要通过channel、加锁,实现多个goroutine的程序执行。

如何复现可能性2?

Go中有个重要的语句:runtime.Gosched() ,它能够让以后的goroutine暂停,退回执行队列,让其余期待的 goroutine运行,目标是为了使资源竞争的后果更显著。

咱们能够代码中任意中央插入这个语句,看看是否合乎预期。

此外,因为咱们曾经剖析出了可能性2,所以有个更容易复现问题的方法:time.Sleep(time.Second * 5)。在容易发生冲突的中央,休眠goroutine五秒钟,期间你能够执行使之抵触的操作,就100%复现问题了!

我也编写了相干代码,参考:github.com/HullQin/go-websocket-examples/commit/e5a5030a。

你能够依照这个步骤复现:

  1. 先进入localhost:8080/123,期待5秒中,直至创立房间实现。
  2. 再新开一个页面,进入localhost:8080/123,不要等5秒,立马执行下一步。
  3. 在5秒中内,敞开第一步的页面。此时,会打印Found room,但该房间曾经敞开了,不合乎预期。预期应该是Create room
  4. 期待5秒完结后,再新开一个Tab,同样进入localhost:8080/123。会打印Create room。而去此时这个页面和上一步关上的页面无奈失常通信。

解决方案(易犯错)

加锁

通过给househub.clients加乐观锁,保障register和unregister不产生竞争。

在serveWs逻辑中,读取house时,就加锁,直到hub.clients更新结束,开释锁。

在hub逻辑中,读取unregister时,也加锁,直到逻辑结束时,开释锁。

代码实现

可参考:github.com/HullQin/go-websocket-examples/commit/e6d32cd4。

减少全局变量:

import "sync"var mutex sync.Mutex

验证

如果你简略这么写,其实会陷入死锁窘境。

因为当43行代码才解锁,然而45行可能先被执行。在45行执行时,hub goroutine就阻塞了,43行代码永远没机会执行了。

之后你会发现音讯发不进来了,因为broadcast也被阻塞了。

最终解决方案

可参考:github.com/HullQin/go-websocket-examples/commit/686a449a。

思考

不要本人阻塞本人,不要让某个goroutine本人给本人解锁,要在其它goroutine解锁。

所以,咱们把register逻辑删掉,不通过channel发送了,间接在serveWs实现这个注册逻辑,并解锁:

hub.go删除register channel即可:

验证

功败垂成!这次,第二个进入后,第一个立马敞开时,不会立刻删除。而是等第二个进入流程结束后,再删除房间。

写在最初

我是HullQin,公众号线下团聚游戏的作者(欢送关注公众号,发送加微信,交个敌人),转发本文前需取得作者HullQin受权。我独立开发了《联机桌游合集》,是个网页,能够很不便的跟敌人联机玩斗地主、五子棋等游戏,不免费没广告。还开发了《Dice Crush》加入Game Jam 2022。喜爱能够关注我 HullQin 噢~我有空了会分享做游戏的相干技术。