« pbc 优化 | 返回首页 | 开发笔记(19) : 怪物行走控制 »

开发笔记(18) : 读写锁与线程安全

最近一段时间,我正解决的技术问题都是和多线程以及并发有关。

我期望在产品上线后,可以用的上至少 32 核的机器。这样,让多核平摊计算负荷就比较重要。单机承载的人数可以不高,但不希望玩家卡在一起特定的操作上。

这里强调的是单机上的并发,而把跑在不同机器的进程会分开考虑。所以,尽量利用共享内存以及单机锁来解决信息交换和状态同步问题。

上个月花了很多时间解决在 Lua 中并发读取同一份配置数据 的问题。一开始是想用一个 Lua State 储存一份结构化数据,然后让其它的 State 对他进行并发的读操作。我以为这种并发读的行为是线程安全的,但是我错了。因为读 Lua State 是需要对 Lua Stack 做修改,所以在没有锁的情况并不能做到线程安全。

随之我实现了一个改进方案。一开始就初始化好若干 Lua Thread ,在不同的 OS Thread 中使用独立的 Stack 空间操作。因为我们的框架只会启动有限的 OS 线程来跑更多的 Lua State ,这样做是可行且线程安全的。不过还是有一点小问题。如果用户向 Lua State 压入并不存在的 key (string 类型) ,修改 Lua 中 string pool 的操作又有线程安全问题了。当然,这个问题还可以用更为复杂的方法规避(比如再给每个线程配置一个独立的 state 做 string 合法性校验)。

我最后放弃了直接用 Lua State 储存这份共享数据,而改用自己实现的一个线程读安全的 hash 表。Lua 仅作为数据解析和加载过程的工具而存在。整套代码我已经开源了


最近一周,实现战斗服务的 Mike 同学又给我提了新需要。

他说,用我之前实现的 sharedb 是可以用来做角色之间的数据交换。但是,如果有一个 buff 的行为会修改角色身上多个属性的值,那么,单单靠这个无法保证线程安全。逻辑上需要多个属性被同时的,原子的,被修改;而不希望在 buff 去修改一组属性的同时,有另外的进程读取这组属性。这样可能造成读到一部分旧版本的属性及另一部分新版本数据。

我们又不想通过 RPC 消息机制来传递这些属性,最终我打算实现一个锁机制,以角色为单位锁定其附属的所有属性,保证读写的原子性。

一开始我把这个问题想的比较复杂。我希望让我们的框架调度器可以了解锁。如果一个写锁被获得时,读锁所在的 Lua Coroutine 可以被挂起。这样一旦写锁被释放,就可以准确的去唤醒相关被挂起的 Coroutine 。这样做涉及实现一个线程安全的队列挂在每个可以被锁的对象上。我想了一整天后,暂时放弃了这个复杂的实现方案。

最终,我决定暂时先实现一个简单的读写自旋锁。不通知框架的调度器,直接在 CPU 一级阻塞。这样对现有代码和接口改动最小。在 Lua 层封装自旋锁的接口时,尽量少允许用户的使用自由。不提供显式的加锁和解锁 api 。而只让传入一小片中间不会被挂起的 Lua 代码运行。

自旋锁依托于 sharedb 工作,让不同的 lua state 间可以获得到同一个角色数据身上的同一把锁,这样就可以方便的同步数据了。

btw, 在读写自旋锁的实现中,gcc 的 Built-in functions for atomic memory access 非常好用。

Comments

:x :twisted: 神云网络2015版最新免费建站方案 http://www.uzsz.cn

难度很大看起来。了解下

使用共享变量的方式做多线程会是恶梦

游戏是不是很难做?

好,我来学习一下。

以角色为单位锁定其所有的附属属性,不光是属性模块吧?其他属于角色的数据呢(比如战斗时buff定时扣除l背包中的物品)?甚至会影响到角色之外的数据?譬如有个buff叫移形换位,不光增加角色的敏捷属性或速度,甚至定时改变你的位置,同时影响角色所在地图视野格子的数据;又比如有个buff定时増加角色属性的同时在角色周围定时刷宠物辅助角色战斗,这些是不是也要保证原子性?更大粒度的锁?如果这些无需保证原子性的话,似乎保证同时更新两个属性原子更新也无必要,除非两个属性必须同时改变否则引起状态不一致可能导致游戏逻辑异常的,换言之,两个属性不是正交的,那么是否可以两个属性合并一个对象,对象內加更小的锁?

建议少用锁和共享内存结构,后面开发中往往遇到痛苦的事情,编码也要十分谨慎。就像osd一样(你懂的)。建议多使用queue或者socket通讯,扩展性,安全性都更好。

不懂lua。我在做的项目中,也在用多核多线程。在mac os xcode开发中,GCD是个绝妙的东西,我用它解决了读写锁和线程安全问题,非常简单实现了。

Thank you for your sharing

牛叉呀~~~~~~~

你期望的单服承载人数是多少?

锁模型不适合在这么多个核里面跑,我觉得还是得用Actor模型,Erlang、Go、Scala那种

@16L
x86也不是原子的

直接在 CPU 一级阻塞,这个阻塞怎么实现的?和系统提供的互斥量一样的挂起还是死等?
@raio x86也不是无锁的,然后双缓冲 多个写入的时候 还是需要锁

最近在学Lua,有个问题始终没找到答案:Lua里面有没有可以做编码转换的库啊?这应该是个很常见的问题吧,例如fetch一个网页是gb的,像保存为utf-8或者像统计字数。

能不能从业务逻辑上就把数据分开。每一份数据都只有一个线程或者进程读写。

比如说角色身上的buff可以由角色所在的地块服务器处理。每个地块服务器对应一个操作系统线程以及一个lua_State,下面有若干coroutine。

这样不就规避了线程同步的问题了吗?

使用共享变量的方式做多线程会是恶梦,bug非常难找,性能也不一定高。
推荐使用消息机制,吞吐量会很高,缺点是会牺牲一定的实时性。

双缓冲机制呢?应该也可以吧,只有一个写入者,其余的都是读取,那么写入的时候只往一个副本上写,写完之后更新配置表的入口指针就OK了,x86机器上更一个指针这种操作应该本身就是原子的吧

我擦 看看

写的非常专业,感谢分享

虽然看不懂,但是感觉有种 oracle DB 里面latch/mutex/spin/burn CPU的那种感觉;觉得如果不经过load testing的话,负载高的时候可能会碰到很多BUG/Performance问题。

没做过游戏,看不懂。不过非常期待你们的新游戏早日上线,从05年不玩大话后就再没碰过网游了,但你的新游戏一定会去玩!

Post a comment

非这个主题相关的留言请到:留言本