原文地址:http://www.intel.com/cd/ids/developer/apac/zho/325610.htm
计算机游戏技术正在经历重大的概念转变:转向多核处理器上运行的多线程引擎。多核处理器为下一代个人电脑和游戏控制台提供动力,游戏开发人员需要将更多平台锁定为目标。遗憾的是,尽管线程执行和跨平台支持至关重要,但许多开发人员发现很难将这些功能用于各自的代码中。本文尝试通过简单的演示应用来研究这两个特点,从而顺利完成这一转换。通过深入了解这些技术,游戏开发人员可以增强对这些技术的理解,并将其实现于所负责的项目之中。
可以在此处找到本文的演示应用。该演示应用程序包括一个在 Windows 上生成和运行的 Microsoft Visual Studio* 2005 解决方案文件,以及一个在 Linux 上生成的文件。运行过程中,将打开一个窗口并绘制一个 OpenGL 场景(图 1).该演示应用以及构成它的代码将应用于本文中的所有示例。
图 1:启动中的演示应用程序
单击此处查看放大图
多线程软件设计是业界长期进行深入研究的一个课题,其基本原理很容易理解。通常,单线程软件以串行方式执行其所有代码。对于游戏,所有的游戏任务都由一个中心循环来执行(处理输入、更新游戏世界、渲染等),每次屏幕上只能渲染一个帧。对于单核处理器,这种串行执行模式已经足够了,但多核处理器中有些处理资源并未完全使用。这些处理资源可以重新加以利用,将游戏任务转换成独立的"线程",而这些线程可以在多核处理器的任何逻辑核上执行。这就是所谓的并行或线程执行模型。并行执行是在目前逐步向多核迈进的处理器上实现高性能游戏体验的关键所在。
该演示应用具有三个线程:一个线程运行基本的事件循环,一个线程更新游戏对象的位置,而另一个线程将游戏渲染到窗口上。窗口底部显示的内容显示了每个线程执行任务的频率,以每秒调用的次数为单位(图 2)。这种单位等效于用于渲染任务的每秒帧数,但对重复执行任务的所有线程应用更为常规的名称。窗口底部的内容还可以显示任务按照串行或者并行顺序执行。按 Tab 键,即可切换至该设置,查看其对每项任务 CPS 的影响。
对于渲染任务和更新任务,可通过按不同的键(对渲染任务为 Z 和 X 键,对更新任务为"."(句点)和"/"(斜杠)键),以交互方式增加或减少其工作负荷。通过调整这些工作负荷,即可模拟那些运行速度受图形或计算限制的游戏了。
一些开发人员发现很难将所有任务的执行速率全部降低,我们是否需要以比游戏更新更快的速度处理事件?这个问题与如何合理地确定游戏所需的线程数量密切相关。但是,相对不同游戏和不同处理环境,得到的答案大相径庭。性能较高的游戏可以充分利用可用的逻辑核,所以在单核处理器上运行时,不会出现速度过慢的情况。因此,在决定游戏所需的线程数之前要检测逻辑核数目。但在现代线程调度系统中运行时,这种优化功能的实际效果却很难超出将处理操作分散为独立任务的模式。该演示应用通过允许用户在一个线程中串行执行所有任务展示了这一点。在单核处理器上,由于开销不足,无法使线程数比逻辑核数目更多。
图 2:关于不断增加的工作负荷的演示应用
ThreadManager:此类用于管理线程池--它是程序开始时创建的一组线程,贯穿程序整个生命周期分配任务。一次性创建线程是为了节省线程创建以及程序执行过程中屡次重建所需的开销。该线程池将每个线程分配一个游戏任务;启动后重复调用同一个函数。应用这种便捷方案,可以方便地启动或停止线程,也可以计算每秒调用次数。"未来工作"(Future Work)章节对其它的线程池管理方案进行了讨论。
ThreadManager 类还为线程相关任务提供了便捷方法。该类将每个线程分配到本地存储区(由此可以将变量与线程本身联系起来),这样就可以根据不同的执行线程运行不同的代码。该演示应用通过该功能来确保,串行主线程上的其他任务或者更改渲染(在全屏之间来回转换)时,渲染功能能够继续发挥作用。ThreadManager 类还定义了允许某线程在一段时间内处于休眠状态或让步于其他线程(如果这些线程正在等待执行)的方法。
该演示应用使用 ThreadManager 子类,称为 ThreadManager 系列(ThreadManagerSerial)。该子类还拥有演示应用在专用线程与主线程之间移动任务时所使用的其他方法。
CriticalSection:这是一个帮助类,在 ThreadManager 和演示应用中创建和管理关键代码段,这些代码段用于防止多个线程同时读取或修改共享数据。
OpenGLWindow:该类使用 SDL(简单直接媒体层)库 1 提供跨平台、固定分辨率渲染上下文。此类消除了使用 OpenGL 渲染时出现的一些不明显的缺点。渲染可以在一个窗口中进行,或采用全屏模式。重新创建窗口时(例如,在转至全屏模式时),OpenGL 无法渲染上下文。为了对此进行控制,该类为线程提供了确定是否有效渲染上下文的功能。这是必要的,因为 OpenGL 要求每个渲染线程与单一渲染上下文相关联,且如果其上下文无效,则不会自动提示线程。
World:此类专用于该演示应用。它可以管理静态的三角形背景以及前景中由演示中的线程任务更新和渲染的一组移动点。背景三角形为渲染任务提供工作负载。前景点用于建立"n 体"(n-body)问题的模型 - 展示万有引力定律。假设每个点都是太空中的一颗行星或小行星,与其他天体互相吸引,一旦碰撞就会结合。太空中心还有一个不可见的黑洞,吸收与其碰撞的所有物质,但最终会"溢出"并释放新的天体。n 体问题相当于更新任务的工作负载问题。
解释演示应用程序输出的关键在于它是否在多核处理器上运行。线程应用仅在单核处理器计算机上运行。但大多数情况下,如果没有多核处理器,以并行方式运行线程任务的程序不会比以串行方式运行相同任务的程序更有效。这一通用规则有一种常见的例外情况,就是因大量网络或磁盘 I/O 的阻挡作用产生的工作负荷。即使在单核处理器上,只要将这些任务线程化,就可以在检索数据的同时执行其他计算。
下面的这些方案,将针对单核和多核执行分别列出预期结果。请注意,在单核中,更改任何任务的工作负荷都将影响到所有任务的 CPS。在多核中,更改某一任务的工作负荷很少甚至不影响其他任务的 CPS。
演示启动时,背景显示 10000,前景显示 50 个点,并以窗口模式运行。下面的所有方案都将这种状态假定为起始状态。按 Z 和 X 可以将三角形每次调整 10000,按 .(句点)和 /(斜杠)可以将物体每次调整 50。按 Tab 键将在串行与并行任务分配间切换。
方案 1:计算绑定执行。按 /(斜杠)键向 n 体问题中添加物体,调整更新任务。添加足够的物体,使更新 CPS 明显少于渲染 CPS,但不得低于 5(如果可能)。在单核计算机上,这很难实现,因为增加更新任务负荷的同时会降低其他两项任务的 CPS。但在多核计算机中,这种状态很容易实现。在这种状态下,物体看上去以波浪状移动,这是对相同数据同时执行渲染和更新任务的负作用。从线程角度看,交互作用并不安全,所以渲染任务会显示每一帧的部分更新。"未来工作"章节详细介绍了线程安全的渲染方法。
请注意更新线程的 CPS,然后按 Tab 键切换到串行执行。在单核中,所有的任务都会将其 CPS 降至同一较低值(或许稍高一点)。在多核中,所有任务都会降至"甚至更低的"数字。这是为什么呢?使用单核时,是在同一个逻辑核上运行所有任务;切换到串行执行后,只会限制较快任务采用与较慢任务一样的速度运行,因此该任务只需与很少的工作竞争。在多核中,缓慢的任务可能已经独立占据一个逻辑核(如无法确定,请参照"未来工作"章节),因此不存在与该核其他任务发生竞争的问题。但在实现任务的串行化时,缓慢任务可能要与其它工作在同一个核上运行,因此速度变得更慢。可以得出这样的结论: 在多核中,以线程方式执行游戏可以加快所有任务的执行。
方案 2:图形绑定执行。按 X 键向背景中添加三角形,调整渲染任务。添加足够多的三角形,使渲染 CPS 介于 5 到 10 之间,更新 CPS 相对更高(在单核上,可能不会很高)。问题是,虽然渲染非常缓慢,但模拟 n 体问题的更新任务被频繁调用,因此可以保持较高的准确性。即便使用较低的渲染 CPS/FPS,也可以跟踪每个物体的路径。
按 Tab 键切换到串行执行。在单核和多核计算机上,渲染任务将使用相同的 CPS,但更新任务只有较低的 CPS。现在更新任务的调用并不频繁,从而降低了模拟 n 体问题的准确性。结果是,即使应用程序每秒渲染相同数目的帧,屏幕上的操作也会更加混乱,更难执行。物体与黑洞的碰撞将加速,但不会被吸进去,而是通过"隧道"持续高速移动。该隧道移动行为就是游戏超缓慢更新的表现形式。在不同类型的游戏中,此问题可能以快照形式出现,将敌人或专注于加固墙壁的玩家所在的阵营尽显无余。推论: 图形绑定游戏也可以受益于更频繁的世界/物理更新。
添加更多平台:MacOS* 显然是一个很好的选择,而且比较容易添加。当然,还可以添加其他平台(控制台、手持设备等)。但平台战略越广泛,就需要越多地考虑界面、控件等问题。
为排序任务创建同步基元:在计算绑定项目中,没有必要渲染重复的帧。渲染线程可以访问由更新线程设置的条件变量(另一种普遍实现的线程 API 功能)。在 ThreadManager 类中提取此功能,使其始终具有跨平台功能。
提供固定的游戏世界更新频率:n 体问题是一项敏感的物理模拟,其准确性受游戏频率长度的影响。选择固定的游戏任务更新频率可以消除准确性偏差。完成游戏更新后,该任务会等待一段时间。
双向缓冲游戏世界更新以便在增大 FPS 的同时实现线程安全渲染:如果项目与计算绑定,则可以将游戏状态平分为静态和动态。对于动态一半的两个副本,游戏世界线程可以更新一个,而渲染线程可以渲染另一个(以及静态世界数据)。这就是许多商业游戏在多核处理器上加快渲染速度的方法。
使用平台特定的线程调度 API:我们可以将线程分配给任何可用的逻辑核,但不保证任何两个线程一定会同时执行。每个平台都有关于如何进行分配以及如何将总运行时间分布到每个线程的策略。有些平台会提供一个控制分配和调度策略的 API。我们可以将这些 API 简化并应用于 ThreadManager 类中。
添加更多的线程任务:AI 循环、动态内容生成、音频、网络等都是游戏项目中常用的功能。其他线程任务可以更好地利用未来将推出的拥有两个以上逻辑核的多核处理器。除此之外,可以修改任务,以充分利用数据并行功能(使用线程将大型任务分解为较小的并行任务)。数据并行功能是使任务数较少的游戏充分利用多核处理器的一种途径。同样的,随着多核技术的发展以及逻辑核数目逐渐控制可并行的任务的数目的趋势,数据并行将成为有效利用处理资源的关键所在。
放弃串行化,使用 ThreadManager 取代 ThreadManager 系列:这样做的目的在于允许任务在等待满足条件时能够使用更多有效的阻塞调用。例如,在 Windows 上,runWindowLoop 函数可能会调用 WaitMessage,而不调用 PeekMessage,阻塞会一直持续到出现需要处理的事件为止。渲染函数可能会调用 glFinish,而不会调用 glFlush,阻塞会持续到全部禎绘制完成。或者,可以修改 ThreadManager 使其具备一次性派送线程池策略(生产商-消费者队列),将线程数与逻辑核数完美地匹配起来,在理论上实现绝对最低线程开销。
Windows:启动 Microsoft Visual Studio* 2005 并打开 TCPGD 目录中的 TCPGD.sln 解决方案文件。选择发布配置,并构建和运行解决方案。构建解决方案时,GLF 项目 2 可能会生成一些警告,但这不会影响应用程序的正常构建和运行。
Linux:进入 TCPGD 目录。构建演示应用,必须单独构建 GLF 和主项目。以下命令将构建和运行该程序:
cd GLF
make
cd ../Main
make
./main
- http://www.libsdl.org* SDL - 简单直接媒体层库。SDL 用于以跨平台方式创建窗口和 OpenGL 渲染上下文。
- http://www.forexseek.com/glf/* GLF - OpenGL 字体渲染库。GLF 用于在演示应用中显示文本。
- 《OpenGL 编程指南》(第五版),作者 Addison Wesley,出版时间 2005 年。该"红宝书"对各级别的 OpenGL 开发都是无价的资源。