kninja 发表于 2024-7-15 18:37

游戏处事器开发指南(三):设计高效的线程模型

大师好!我是长三月,一位在游戏行业工作多年的老法式员,专注于分享处事器开发相关的文章。
一周一次的系列分享又与大师见面了。此次的主题是设计高效的线程模型,是并发类别下的第一篇。
在游戏处事器开发中,一个高效的线程模型可以充实操作多核能力,将CPU的操作率阐扬到极致,而且降低请求响应时间,带给玩家尽量低的延时体验。
在许多场所需要评估是使用一个线程还是多个线程。适合分拆成多线程执行的场所是:多个任务彼此独立性高,而且单线程串行执行可能达到单核能力上限的瓶颈。下面是一个关于开房间战斗的例子,每个房间都有若干玩家一起战斗,房间之间彼此独立。原始的设计是使用单线程执行所有战斗房间的计算任务:


这样设计的问题是,由于战斗计算是CPU密集型的计算任务,当房间数量增多到必然程度时会达到单核性能瓶颈,无法在原定的一帧时间内执行完所有的房间计算,导致掉帧的情况呈现。解决法子是开启多个线程分管计算压力,每次有新房间创建就投放到一个最空闲的线程中打点。因为房间之间彼此独立,所以也不需要考虑任何同步相关的问题:


同样典型的场景还有MMO中的大地图,由于整个地图运算量巨大,凡是会考虑使用多线程分管计算量,拆分角度凡是是按地图区域,每个区域分配一个线程,或者是按业务类型,每种独立的业务逻辑分配一个线程。
不外,并不是所有的任务都适合分拆到多线程。使用多线程对比单线程,至少有以下错误谬误:

[*]需要更多地考虑线程之间同步的问题。这对于法式员带来了额外的开发承担,措置得不好可能导致死锁或状态错误等严重问题。而且如果同步粒度太大,会使多线程对性能的提升变得很有限。
[*]多线程线程切换会带来额外性能开销,此外每个线程都要分配独立的仓库空间。
以下是一个反例,说明关联性太强的多个任务使用多线程会遇到的同步问题,及其对性能的影响。
在Java游戏处事器中,从网络获取的玩家请求颠末解析,投放到业务线程中进行措置。我们为来自同一个玩家的多个请求添加玩家锁,目的是但愿它们串行执行,避免多线程造成的竞争问题。这样做的成果是,来自同一个玩家的多个请求虽然投放到线程池中多线程执行,但是每个请求都要等待前一个请求执行完并释放玩家锁,在此之前都是阻塞的。考虑一个更极端的情况,玩家登陆时凡是会同时调用一系列接口,当调用接口数大于线程池中线程总数,而且第一个请求因异常情况阻塞时,此时线程池中所有线程都被阻塞,无法措置其他玩家的请求。


解决这个问题的法子是避免同步,改为将每个玩家绑定到一个线程上,玩家请求放入线程专属的任务队列依次执行。绑定的策略可以是对玩家id取模:
分配到的线程id = 玩家id % 线程总数

以上模型虽然能避免同步带来阻塞的问题,但是可能在部门线程上存在性能热点。例如,恰好玩家分配到线程1上的数量较多,而且来自这部门玩家的请求数也较多,就会造成线程1繁忙,而其他线程空闲的情况。
我们对上述模型进一步优化。上述模型的问题是没有操作到线程池自动分配任务到空闲线程的功能。因此我们改成为每个玩家维护一个请求队列,该队列每次将一个新任务投放到线程池中,具体投放到哪个线程由线程池来决定,等这个任务执行完后,再通知该玩家的请求队列投放下一个请求。
这样既避免了多线程同步的问题,也避免了部门线程上的性能热点。


设计线程模型时,另一件要注意的事是:尽量按业务类型划分分歧的线程池,不要共用一个。共用一个线程池的坏处是,当一个业务遇到异常情况(例如网络或者数据库IO阻塞),导致线程耗尽时,可能会影响到此外业务。而且多个业务放在一个线程池中也不利于排查问题。
一个典型的游戏处事器凡是有以下类型的线程池:

[*]网络IO:负责读写网络动静,并措置网络数据与用户请求之间的转换。
[*]用户请求措置:也叫业务线程池,措置类型1中解析出的用户请求。
[*]按时任务:分为按时任务的打点线程和执行线程,前者负责按时任务的添加、删除和触发,凡是一个线程就够了,后者一般使用线程池。
[*]异步SQL:在异步存储SQL的场所使用。
[*]对外通信:用于与第三方处事器异步通信,避免阻塞业务线程。
最后,分歧的并发模型对线程模型的设计也会有影响。以下是三种常见的并发模型:
1. 同步和共享内存:代表有Java。这是最常见的并发模型,通过同步来控制并发,直接使用线程池。对于法式员措置线程同步和设计线程模型有较高的要求。
2. Actor:代表有Skynet、Erlang、Akka。基本思想是:分歧的Actor维护本身独立的数据,Actor之间通过异步动静通信,避免直接调用;Actor维护和措置本身的动静列表;只有每个Actor才允许写本身的数据,保证写独一性。Actor的好处是从模型上避免了考虑同步的承担,错误谬误是模型更复杂,需要必然的经验才能掌握。
3. CSP:代表有Golang。核心要素是协程和通道。CSP与Actor类似,区别在于Actor之间是直接通信的,而CSP的通信是面向通道的;Actor之间通信是异步,而CSP从通道读写数据是同步的;在Actor模型中,动静队列存在于每个Actor实例中,而CSP中动静队列存在于通道中(通道带缓存的前提下)。与Actor模型类似,通道让法式员省去了考虑同步的承担,而协程让编写高效的并发代码更加容易。首先,协程是比线程更轻量级的调剂单元,一个进程中可以开启成千上万个协程;其次,协程遇到阻塞时会从逻辑措置器中卸载直至从阻塞恢复,在此期间逻辑措置器又会新建线程措置此外协程任务,不会呈现直接使用线程池时遇到的线程全阻塞的问题。有了协程,法式员可以不用与线程直接打交道,也省去了上述使用线程池遇到的诸多懊恼。
总之,一个高效的线程模型可以充实榨干多核的性能。对于独立性高的多个计算密集型任务,使用多线程可以避免单核的性能瓶颈。对于互相依赖的多个任务,我们应该仔细设计线程模型,避免同步和单点过热对性能的影响。除了共享内存,还有Actor和CSP两种并发模型也很常见,它们各有特点,为编写高效的并发法式带来了便当。即使没有实际用到它们,熟悉和借鉴分歧并发模型的思想也是有益的。
页: [1]
查看完整版本: 游戏处事器开发指南(三):设计高效的线程模型