协程锁是猫大设计发明的一种语法糖,优雅地以队列的方式管理同锁号协程的执行顺序,以保证某一段异步代码的原子性,因此行为很像加锁。
锁相关这东西其实一点也不高深,有一些术语很恶心,比如竞争,比如破坏死锁的n个条件,只会让你更绝望。其实你换个问法就理解了:A,B两个操作竞争账号数据=A,B两个操作都在读写账号数据。你思考问题从“读写”触发,审核考量“操作”,100%能想明白用锁问题。还不明白,举个例子:登录,登出这两个操作都会修改账号数据的在线状态(在线,下线),而好友模块又会读取好友的在线状态,我们首先应该想,登录登出这俩操作必定竞争,因此必须上锁。好友模块看需求而定:如果策划要求好友在线状态必须精确,那么读的时候,锁也要加上,否则这种边缘系统,不加锁也无所谓。
还不明白?再举个例子。玩家的背包,添加物品和消耗物品,必然是对背包进行写操作,因此必须加锁。新需求是查询某个礼包物品的倒计时,算是个边缘读需求,加不加锁无所谓。又来一个新需求,整理背包,那么对背包进行读写操作,因此必须加锁。
总而言之,写锁竞争一定要加锁,读锁之间无竞争一定不要加锁(浪费性能,增加无意义等待时间),读写竞争看需求,要求精准(且愿意付出等锁耗时的代价)就加锁,否则就不加锁。不要将锁上升到同步异步的高度,同步的写入也会跟异步的写入产生竞争,不要认为自己写的是同步代码就高枕无忧。只看读写权限,算是再三解释了
这个特性不仅在后端非常有用,也分享一些前端使用经验。
案例1:诸如GameFrameWork等框架,喜欢用一个FSM来管理APP的生命周期,例如LoginState,LobbyState,BattleState,给你提供标准状态机生命周期Enter/Exit 来方便你写代码。但是往往这些流程的Enter阶段是不可以打断的,举个例子,你Login了一半,突然外边调了一下ChangeStateToBattle,此时Login流程的原子性被打破,会造成各种问题。这时,我们对状态机的ChangeState函数加锁,即可让这些调皮的调用竟然有序:即使在Login流程中被调用了ChangeToBattle,那么协同锁也会保证完整走完Login流程,才会进入Battle!
案例2:讨厌的PopUp对话框。客户端一瞬间收到N条服务器消息,要求展示对话框,并且一条一条展示,用户关闭了一个,下一个才弹出来。伴生需求只会比这个更恶心,例如充值成功类对话框不受这个限制,断线提示对话框高于一切等等。更要命的是,有些策划要求对话框有关闭动画,谁写过谁想吐。但是在协同锁的帮助下,我们只需要对每种弹出规则加锁即可享受队列,想等待对话框关闭,也只需在协同锁的锁块代码内等待即可,非常好写。
案例3:底层资源加载。对于一个AssetBundle,我们不想向资源引擎(例如YooAsset)提交重复加载内容,所以简单对这个AssetBundle的加载进行加锁,享受协同锁的队列特性,即可优雅完成需求。Demo中亦有不少此类用法。
案例4:其实和案例3的底层原理是一致的,作为一个例子的Case来补充。我们经常会有异步加载并且填充Sprite的需求,更要命的是,这个Sprite的图可能是从网上下的,我们有义务保证不重复请求。那么对Sprite加载并填充,亦可以使用协同锁
在发现好的案例,会更新文档