异步编程设计目标和改进
本主题介绍为 Microsoft 游戏开发工具包 (GDK) 创建新的异步编程模型时制定的设计目标。 本主题还介绍了根据开发人员反馈对 Xbox One 软件开发包模型所做的改进。
本主题内容
简介
Microsoft 游戏开发工具包 (GDK) 引入了新的异步编程模型,旨在提供高度可自定义和可控的执行行为,同时具有较低的处理和内存开销。 Microsoft 游戏开发工具包 (GDK) 中的所有异步 API 调用均使用此模型来提供一致性。 但是,也可以在任务和异步系统中直接使用异步库来管理任务的排队和调度。
Microsoft 游戏开发工具包 (GDK) 中的新解决方案借鉴以前的 Xbox One 软件开发工具包异步模型和开发人员的反馈,向你提供对异步任务运行方式和位置的完全控制。 可以使用任何线程处理设计,无论是针对单个线程同步执行,还是在具有特定高优先级和关联掩码的游戏线程上并行执行。 Xbox One 软件开发工具包只使用了系统线程池。
Microsoft 游戏开发工具包 (GDK) 范例转换通过使用 Win32 样式的 flat-C 或简单的 C++ API 来改进 Xbox One 软件开发工具包解决方案。 通过此改进,可以直接管理内存分配和解除分配计时。 可以更轻松地检查性能,并且大多数 API 调用(和用于执行任务的所有异步库 API 调用)可提供高性能保证。
设计目标
异步库的设计中有一些关键目标可确保在使用 API 或系统时,游戏可获得最佳性能,且只需要最低的开销。 以下列表包含一些高级别目标。
- 异步代码是一致的,且易于理解和识别。
- 你可以完全管理回调执行行为。
- 还可以按任何所需方式使用线程。
- 性能和内存使用情况是一致的,且易于管理。
- 运行任务的系统开销较低,且在主线程上可用。
- 不应只为使用 Microsoft 游戏开发工具包 (GDK) API 而保留异步库。
一些目标在下面的子部分中进行介绍,其他目标则在本主题的 Xbox One 软件开发工具包的改进部分中介绍。
一致和跨平台
一致性可为异步 API 的编写和行为方式带来以下预期。
- 方法和数据以
X
为前缀,后接库名称。 - 通过 HRESULT 错误代码报告。
- Microsoft 游戏开发工具包 (GDK) 中的所有异步调用均使用 XAsyncBlock。
- 异步 Microsoft 游戏开发工具包 (GDK) 启动方法以
Async
为后缀。 - 异步 Microsoft 游戏开发工具包 (GDK) 结果方法以
Result
为后缀。
跨平台意味着可以在 Microsoft 游戏开发工具包 (GDK) 面向的任何平台上使用相同的异步 API 调用。 这些平台目前包括 Windows 电脑(NDA 主题)要求授权,Xbox One 系列主机和 Xbox Series 主机(NDA 主题)要求授权。
无论使用 Microsoft 游戏开发工具包 (GDK) API 调用的异步库还是自己的功能,该进程对于设置 XAsyncBlock 和启动异步方法来说始终相同。 每当库提供异步方法时,这些方法都将使用异步库来实现。 下表列出了一些已公开异步方法的库。
开发人员的完全控制
新异步库的一个重要部分在于,可让开发人员实现完全控制以便为游戏提供最佳用法。 这意味着模型的许多部分(包含以下部分)是可自定义的。
创建任务队列时,可以自定义两个端口的回调的调度方式,如下表所示。
端口模式 | 行为 |
---|---|
ThreadPool |
在系统池线程的后台中,排队的回调会自动、并发调度。 |
SerializedThreadPool |
在系统池线程的后台中,排队的回调会自动调度。 但是,一次只能激活一个回调,以确保串行执行不会发生重叠。 |
Immediate |
尝试排队时,回调将在调用方的堆栈帧中立即运行,而不是排队等候。 如果工作端口是 Immediate ,则会在开始异步调用时运行工作回调。 如果完成端口是 Immediate ,则在同一堆栈帧中完成工作回调后将运行完成回调。 |
Manual |
任何在此模式下排入端口队列的回调均不会自动调度。 必须调用 XTaskQueueDispatch 才能在端口上手动执行回调。 调用模式决定回调的线程和并发方式。 |
如果未使用 XTaskQueueSetCurrentProcessTaskQueue 指定任何任务队列,则可以替换游戏使用的默认任务队列。
如果想要确保始终指定任务队列,还可以将该函数与 nullptr
一起使用来删除默认队列。
执行此操作时,如果未指定任务队列,会生成错误。
如果更喜欢使用轮询来进行异步调用以了解任务何时完成,可以使用 XAsyncGetStatus 检查返回代码。
任何线程处理情况
将任务队列端口配置为使用手动调度时,将不会限制端口的调度时间或方式。 异步库在其实现中是线程安全的,可在无锁定的情况下执行自定义线程处理和并发行为。 临界区、联锁操作和其他相关的多线程处理结构仅在保护回调中使用的用户数据而非异步系统时才是必要的。
使用自己的线程时,可随意设置关联掩码和线程优先级以满足任何实现需求。 还可使用 Windows 消息循环处理调度。 手动端口内的任务回调执行行为完全取决于调用 XTaskQueueDispatch 的线程的设置方式,如图 1 和图 2 所示。
图 1. 显示一个线程的回调执行行为
图 2. 显示多个线程的回调执行行为
对 Xbox One 软件开发工具包所做的改进
响应你的反馈并改进快速解决方案对于 Microsoft 游戏开发工具包 (GDK)(NDA 主题)来说非常重要要求授权。 以下部分列出了相较于 Xbox One 软件开发工具包中的异步编程所做的一些关键改进。
不再使用 C++ /CLI (WinRT)
Xbox One 软件开发工具包需要使用 C++/CLI (WinRT) 来实现异步函数行为。 这样,系统就管理对 Xbox 服务和其他服务的调用,而不会遇到资源泄漏和错误等问题。 但是,这还要求掌握新编码语法,以及考虑系统的隐式时间切片和资源使用情况。
Microsoft 游戏开发工具包 (GDK) 异步模型要求授权,转换为异步库的 Win32 风格的 C 风格 API 模型。 Microsoft 游戏开发工具包 (GDK) 代码可以直接在 C/C++ 项目中使用,而无需集成托管的 WinRT 生态系统。 结果是,系统线程上的所有分配和行为都彼此独立且被充分了解,并且只需最小程度的后台行为。
线程处理行为的完全控制
Xbox One 软件开发工具包有一种处理异步调用的单一方法,即在系统池线程上并发运行它们。 控制范围仅限于调整系统线程池。
在 Microsoft 游戏开发工具包 (GDK) 中,线程处理可像针对 Xbox One 软件开发工具包描述的那样使用系统线程池,或通过手动线程处理进行自定义,以便根据应用程序的需要来生成行为。 通过手动线程处理,回调执行行为由调用 XTaskQueueDispatch 的方式和时间确定。 如果仅对一个线程调用此函数,则该行为将变为在该线程上同步运行。 如果多个线程同时调用该函数,则每次调用都会从任务队列获取不同的回调并以并行方式运行它们。
本主题的开发人员的完全控制部分中介绍了不同的回调执行行为。
错误代码,而不是异常
Microsoft 游戏开发工具包 (GDK) 包含许多技术(新技术和旧技术都有)。 因此,不同库的错误处理方式可能不同。 异步库的设计使其“不会发生异常”。 这意味不对调用错误、流控制、状态报告或任何其它运行时用途使用异常。 可在游戏中使用自己的异常处理方式。
现在,大多数错误和状态代码均通过 HRESULT
代码(采用标准 Win32 编码风格)报告。 当结果状态代码不重要时,可以使用布尔值。 当预期 API 方法会生成常见错误时,将在文档中列出这些错误并说明原因。
由于使用 HRESULT
代码,因此检查代码的一个简单模式是使用 Windows SUCCESS()
和 FAILED()
宏。 检查特定错误代码以确定后续措施时,可直接检查 HRESULT
值。
有关 Microsoft 游戏开发工具包 (GDK) 中的错误处理的详细信息,请参阅 Microsoft 游戏开发工具包中的错误处理。
无垃圾回收
由于 Microsoft 游戏开发工具包 (GDK) 中不支持任何 C++/CLI (WinRT),这意味着在运行时期间后台不会发生垃圾回收。 所有跟踪的 OS 分配都由直接 HANDLE
对象或由库提供的自定义句柄 typedefs 表示。
可通过使用句柄来设置由调用方管理的分配/解除分配计时、行为和性能。 可将这些分配和解除分配转移到支持性加载程序线程,以避免性能故障。 当性能至关重要时,不会发生意外解除分配。
但需要执行资源跟踪和避免内存泄漏。 确保在不再需要分配的句柄时将其解除分配。
保证高性能
Xbox One 软件开发工具包异步模型及其使用托管的 COM 框架使其可产生意外的内存和性能问题,这些问题有时很难归因。 不过,Microsoft 游戏开发工具包 (GDK) 模型可确保性能和内存占用的一致性。
由于线程处理可控,使用探查器或自定义代码可以更容易地检查系统调用的性能。 ATG 异步编程示例具有内置测试,可测量某些方面的所用时间。 图 3 中的图表测量了使用自定义异步提供程序管理异步任务所花费的时间。 用于测试的自定义线程会尽可能快地执行回调。
图 3. 显示测量异步系统开销的图表
系统开销非常低。 开销成本主要在等待线程调度任务队列端口时产生。 总异步任务时间(忽略开销成本)是任务回调中的实际工作时间。
时间敏感线程
时间敏感线程是一种新规范,Microsoft 游戏开发工具包 (GDK) 用它来识别不希望发生任何阻塞或意外长时运行的操作的游戏线程。 用户可以通过调用 XThreadSetTimeSensitive 来将游戏线程标记为时间敏感线程。 将线程标记为时间敏感线程时,如果对该线程的任何 Microsoft 游戏开发工具包 (GDK) API 方法调用不能提供一致和可靠的运行时性能(定义为不具有“时间敏感安全性”),则将为你触发调试断言。
这些时间敏感安全方法在实现方面提供以下保证。
- 不会执行按需加载或初始化。
- 不会跨 VM 或进程边界进行调用。
- 内存分配受到限制。
这意味着,时间敏感安全方法应在一致的时间以相同的输入运行,并且不会导致性能意外降低。 许多 Microsoft 游戏开发工具包 (GDK) 方法在内部标记为时间敏感安全方法,线程无需标记为时间敏感安全来通过调用时间敏感安全方法获取保证。
大多数异步 API 方法都是时间敏感安全方法。 不具有时间敏感安全性的方法与任务队列设置以及通常在加载或初始化时间完成的其它低频率调用相关。 与启动、管理、取消、完成或直接处理异步任务相关的所有方法都是时间敏感安全方法。 有关不安全函数的详细信息,请参阅时间敏感线程主题中的时间敏感线程的不安全函数部分。
Microsoft 游戏开发工具包 (GDK) API 调用之外的通用用法
Microsoft 游戏开发工具包 (GDK) 异步库是通用的,且不局限于 API 调用。 Microsoft 游戏开发工具包 (GDK) 提供的任何异步调用都将在内部使用异步库,并为包含启动任务和获取结果在内的操作提供一致的方法。 有关将库用于非 GDK API 用途的详细信息,请参阅示例代码。
通过此设置,启动异步任务的过程是相同的,无论是 Microsoft 游戏开发工具包 (GDK) 函数还是自定义模式:为任务设置 XAsyncBlock,然后启动任务。 启动任务可以是 Microsoft 游戏开发工具包 (GDK) API 函数,也可以是自定义游戏方法。
开发人员随时可在游戏中利用异步库来实现任何异步用途。