Skip to main content
Microsoft Game Development Kit (GDK) 实现了一种新的异步 API 模式,该模式针对我们从游戏开发人员那里收到的关于 XBOX One ERA 编程模型中所实现异步模式的反馈进行了改进。我们的目标是让这种新模式更易于集成到典型的游戏架构中,并为游戏开发者提供他们所期望的高度控制能力。本主题介绍这种设计模式,并对可用于实现异步模式的库提出建议。

概念模型

Microsoft Game Development Kit (GDK) 中的异步编程分为两个主要组件:任务(Task)和任务队列(Task Queue)。虽然库中还有更多功能,但整个概念模型都围绕这两个主要组件展开。 任务是一组可以启动、检查状态、可能取消、完成并返回其完成信息的异步工作。对于 Microsoft Game Development Kit (GDK) 模型,任务由两部分组成:工作回调和完成回调。这允许更强的控制能力,例如完全的并行处理,或者并行工作与单线程完成相结合。 任务队列是一个容器,用于将工作回调和完成回调入队以便稍后执行。任务队列中有两个内部队列,称为端口,分别处理工作回调和完成回调。它们被称为工作端口和完成端口。 图 1. 任务和任务队列示意图 任务队列的每个端口都是在创建时不同地配置的,以创建不同的回调执行行为。例如,工作端口可以配置为异步执行,而完成端口可以配置为在主线程上串行运行。可以设置手动模式以完全控制执行行为。端口配置模式在下方进行了说明 启动异步任务时,回调不会立即入队到任务队列。异步提供程序负责处理状态变化,以确保在完成回调入队并分派之前,工作先被入队并分派。 任务队列本身不直接处理线程。相反,它依赖外部调用来分派其端口。外部调用决定了线程和并发行为。任务队列本身是完全线程安全的。 图 2. 端口被分派到多个线程 基本上就是这样!任务的回调被入队到任务队列的工作端口和完成端口,然后任务队列以某种方式分派这些回调。API 包含了一整套用于管理任务队列、检查回调状态、跟踪工作数据、创建自定义任务处理等功能。 Microsoft Game Development Kit (GDK) 异步 API 调用始终在内部实现工作回调,而完成回调始终是可选的。对于 Microsoft Game Development Kit (GDK) 异步调用之外的使用,你必须提供工作回调。

要求

游戏开发者列出了 API 调用的以下要求。
  1. 优先使用同步调用而不是异步调用
  2. 提供带轮询的异步
  3. 提供带回调的异步
  4. 能够控制异步工作在哪个线程上执行
  5. 能够控制完成回调在哪个线程上执行

API 类型

Microsoft Game Development Kit (GDK) 在其 API 设计中力求简单明了。游戏开发者是精细调整代码以最大限度利用硬件的专家。我们尽可能地给予他们控制权。API 实现分为以下类型。
  • 时间敏感安全: 时间敏感安全的 API 是指可以在时间敏感的线程上调用的 API。请注意,虽然这通常意味着 API 很简单或非常快速,但关键概念是 API 的性能特征是一致的。它们始终是同步的,永远不需要异步版本。这些 API 应被记录为时间敏感安全的。
  • 非时间敏感安全: 这些 API 不适合从渲染线程调用。它们的性能特征可能差异很大。大多数 API 属于这一类。
  • 异步: 这些 API 本质上是异步的,例如 Web 服务调用。它们使用本主题中描述的异步模式。异步 API 在 Microsoft Game Development Kit (GDK) 中不像在 XBOX One ERA 编程模型中那样常见——异步 API 通常是长时间运行且可取消的。除少数特定使用场景外,异步 API 都会有一个非时间关键安全的同步版本。调用异步 API 应始终是时间关键安全的。
  • 通知: 通知本质上是周期性的,没有明确的结束。它们与异步 API 相关,但由于其周期性,它们对开发者来说应该看起来和表现得有所不同。注册通知应始终是时间关键安全的。

异步 API 模式

Microsoft Game Development Kit (GDK) 引入了一种通用的异步 API 模式,Microsoft Game Development Kit (GDK) 组件可以使用它来提供一致的异步支持。其核心是一个类似于 OVERLAPPED 的结构体,称为 XAsyncBlock
XAsyncBlock 是一个由调用方提供的结构体。调用方在此结构中填写可选字段,如下表所示。 Internal 字段由系统使用,不应被修改。此结构中用户可设置的字段在异步操作期间不应被修改。XAsyncBlock 必须在异步操作的生命周期内保留在内存中。如果 XAsyncBlock 是动态分配的,则完成回调是最早可以删除它的时机。 除了 XAsyncBlock,还有少量辅助 API,如下所示。
XAsyncGetStatus 返回异步调用的状态。当调用开始时,此状态为 E_PENDING。完成时会变为 S_OK 或特定错误。如果调用被取消,则返回 E_ABORT。 XAsyncGetResultSize 返回获取调用结果所需的缓冲区大小。实际获取结果的 API 针对每个异步调用进行了定制。 XAsyncCancel 可用于取消调用。取消由被取消的操作决定,可能是同步的、异步的,或根本不发生。如果操作被取消,则 XAsyncGetResultXAsyncGetResultSizeXAsyncGetStatus 会返回 E_ABORT。已取消的调用会向 XAsyncBlockXAsyncCompletionRoutine 参数发出信号并调用其回调。 XAsyncRun 是一个辅助方法,可以异步运行任何代码。

异步 API 使用

首先,让我们看看以下代码示例中的同步 API。
此 API 调用 Web 服务以确定还剩多少存档存储空间。要添加异步支持,我们声明一对新 API。
如果异步调用已启动,XGameSaveGetRemainingQuotaAsync 会返回 S_OK(因为此 API 只有异步版本,返回 E_PENDING 没有意义)。在调用完成之前,XGameSaveGetRemainingQuotaResult 返回 E_PENDING。 让我们看看下面的实际使用情况。
XAsyncBlocks 全都需要一个任务队列(如下所述),任务队列控制异步调用的执行位置和方式。如果未提供任务队列,则使用进程范围的任务队列。 请注意,XAsyncBlock 需要在异步调用的生命周期内保留在内存中。在此示例中,它是动态分配的,并在完成回调中删除。它也可以存储为全局变量或成员变量。如果同一个 XAsyncBlock 同时被用于多个异步调用,则行为未定义。 XGameSaveGetRemainingQuotaResult 完成了异步调用的整个循环。它释放异步块中的内部数据,因此该块现在可以用于新的调用。对 XGameSaveGetRemainingQuotaResult 的后续调用将失败。XGameSaveGetRemainingQuotaAsyncXGameSaveGetRemainingQuotaResult 也在异步块内成对使用——如果你将一个异步调用与另一个结果 API 不匹配地混用,就会发生错误。 如果异步调用没有数据负载,意味着只有 HRESULT 状态很重要,那么定义一个只接受异步块的 Result 方法,如下所示。

控制工作分派

前面调用中的异步工作是在哪个线程上完成的?完成回调是哪个线程调用的?这是由分配给 XAsyncBlock 的任务队列决定的。 任务队列有两个“端口”:工作端口完成端口。每个端口都有一个分派模式,用于决定入队到该端口的回调如何处理。有几种分派模式。
  • 线程池: 入队到线程池队列的回调在系统线程池上执行。线程池会并行调用这些回调,当线程池线程可用时依次从队列中取出一个调用来执行。
  • 串行线程池: 回调入队并在线程池上运行,但每次运行一个。
  • 手动: 入队到手动队列的回调不会自动分派。开发者可以自行选择在任何想要的线程上分派它们。
  • 立即: 立即分派模式根本不入队。它会立即在提交回调的线程上执行调用。
存在一个默认的进程任务队列,其工作端口和完成端口都通过系统线程池进行分派。如果 XAsyncBlock 中未传入任何队列参数,就会使用此进程任务队列。游戏也可以禁用进程任务队列,要求向 XAsyncBlock 传入一个队列。 我们预期许多开发者会选择手动分派模式,以完全控制异步工作和完成回调的执行时机和位置。 有关任务队列的详细信息,请参阅异步任务队列设计

通知

通知可能没有结束,并且可能被调用多次。通知应支持异步调用要求的一个子集。
  1. 带轮询的异步
  2. 带回调的异步
  3. 控制回调发生的线程
通知使用任务队列以便开发者控制回调线程,但除此之外不使用异步块——它们的设计更像是带有 RegisterUnregister 方法的标准事件。
  • 一个 Register 方法,它接受任何调用特定的参数、一个任务队列、一个可选的 void 上下文以及一个强类型的回调指针。最后一个参数是一个输出参数,用于返回一个令牌。
  • 一个 Unregister 方法,它接受任何调用特定的上下文和令牌。
  • 通过添加一个与通知回调无关的单独方法来支持轮询。
让我们看看下面这个可能获取 Windows 消息的示例。
请注意,在此示例中,UnregisterMessageAvailable 接受最后一个 “wait” 参数并返回一个 bool。这允许调用方决定在调用被触发时如何处理注销。

异步库

为了更容易创建支持异步模式的一致 API,我们提供了一个库,可用于实现 API 的“异步基础设施”。该库的 API 如下所示。
此 API 使用单个回调,并结合一个操作值来指示调用 API 的原因。还有一个单独的数据结构,在调用推进过程中会被填充。要使用此 API,请执行以下操作。
  1. 使用调用方传入的异步块调用 XAsyncBegin,并提供一个包含实现的回调。
  2. 为该调用执行异步工作。如果你需要在工作线程上运行工作,请调用 XAsyncSchedule。如果你可以使用操作系统异步原语来执行工作,并且能够足够快地设置这些原语以保持时间关键安全,那更好。
  3. 如果你需要从工作线程回调中调用其他异步工作,可以从工作线程返回 E_PENDING。你也可以在工作线程内部调用 XAsyncSchedule 来重新调度更多工作。
  4. 当所有工作完成时,调用 XAsyncComplete
  5. 提供一个围绕 XAsyncGetResult 的强类型包装器来返回结果。
  6. 如果你的异步调用没有数据负载,你应该提供一个围绕 XAsyncGetStatus 的强类型包装器,并将所需的缓冲区大小设为零传递给 XAsyncComplete
异步提供程序回调会通过以下操作调用。
  • BeginXAsyncBegin 期间以此操作码调用异步提供程序。如果提供程序实现了此操作码,则应通过调用 XAsyncSchedule 或通过外部方式启动其异步任务。此回调在 XAsyncBegin 调用链中同步调用,因此永远不应阻塞。
  • DoWork 在通过任务队列调用 XAsyncSchedule 来调度异步工作的情况下调用。提供程序函数执行其需要的任何工作。完成时,它会以结果代码和数据负载大小调用 XAsyncComplete,如果调用没有数据负载,则大小可以为零。如果需要执行更多异步工作,提供程序可以调度该工作并应返回 E_PENDING。
  • GetResult 被调用以获取调用的结果。因为数据大小在调用完成期间传递给 XAsyncComplete,所以此处不需要参数检查——所有缓冲区和缓冲区大小都已由库验证。
  • Cancel 当用户取消异步调用时调用。如果调用可以取消,就取消它并以 E_ABORT 作为结果代码调用 XAsyncComplete
  • Cleanup 当调用完全结束时调用,提供程序可以删除任何动态内存。
异步提供程序只需要实现它所需的操作。例如,没有清理需求的不可取消异步 IO 只需要实现 GetResult 以下是一个 FactorialAsync 方法的示例,它异步实现阶乘。

参考 API 文档

另请参阅

异步编程设计目标和改进 异步任务队列设计
最后修改于 2026年8月24日