我是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 goroutine
和Write 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协程,协程的执行程序是开发者很容易通过async
和await
语法掌控的。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。
你能够依照这个步骤复现:
- 先进入
localhost:8080/123
,期待5秒中,直至创立房间实现。 - 再新开一个页面,进入
localhost:8080/123
,不要等5秒,立马执行下一步。 - 在5秒中内,敞开第一步的页面。此时,会打印
Found room
,但该房间曾经敞开了,不合乎预期。预期应该是Create room
。 - 期待5秒完结后,再新开一个Tab,同样进入localhost:8080/123。会打印
Create room
。而去此时这个页面和上一步关上的页面无奈失常通信。
解决方案(易犯错)
加锁
通过给house
和hub.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 噢~我有空了会分享做游戏的相干技术。