概念模型
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 调用的以下要求。- 优先使用同步调用而不是异步调用
- 提供带轮询的异步
- 提供带回调的异步
- 能够控制异步工作在哪个线程上执行
- 能够控制完成回调在哪个线程上执行
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:
Internal 字段由系统使用,不应被修改。此结构中用户可设置的字段在异步操作期间不应被修改。XAsyncBlock 必须在异步操作的生命周期内保留在内存中。如果 XAsyncBlock 是动态分配的,则完成回调是最早可以删除它的时机。
除了 XAsyncBlock,还有少量辅助 API,如下所示。
异步 API 使用
首先,让我们看看以下代码示例中的同步 API。控制工作分派
前面调用中的异步工作是在哪个线程上完成的?完成回调是哪个线程调用的?这是由分配给 XAsyncBlock 的任务队列决定的。 任务队列有两个“端口”:工作端口和完成端口。每个端口都有一个分派模式,用于决定入队到该端口的回调如何处理。有几种分派模式。- 线程池: 入队到线程池队列的回调在系统线程池上执行。线程池会并行调用这些回调,当线程池线程可用时依次从队列中取出一个调用来执行。
- 串行线程池: 回调入队并在线程池上运行,但每次运行一个。
- 手动: 入队到手动队列的回调不会自动分派。开发者可以自行选择在任何想要的线程上分派它们。
- 立即: 立即分派模式根本不入队。它会立即在提交回调的线程上执行调用。
通知
通知可能没有结束,并且可能被调用多次。通知应支持异步调用要求的一个子集。- 带轮询的异步
- 带回调的异步
- 控制回调发生的线程
- 一个 Register 方法,它接受任何调用特定的参数、一个任务队列、一个可选的 void 上下文以及一个强类型的回调指针。最后一个参数是一个输出参数,用于返回一个令牌。
- 一个 Unregister 方法,它接受任何调用特定的上下文和令牌。
- 通过添加一个与通知回调无关的单独方法来支持轮询。
异步库
为了更容易创建支持异步模式的一致 API,我们提供了一个库,可用于实现 API 的“异步基础设施”。该库的 API 如下所示。- 使用调用方传入的异步块调用 XAsyncBegin,并提供一个包含实现的回调。
- 为该调用执行异步工作。如果你需要在工作线程上运行工作,请调用 XAsyncSchedule。如果你可以使用操作系统异步原语来执行工作,并且能够足够快地设置这些原语以保持时间关键安全,那更好。
- 如果你需要从工作线程回调中调用其他异步工作,可以从工作线程返回 E_PENDING。你也可以在工作线程内部调用 XAsyncSchedule 来重新调度更多工作。
- 当所有工作完成时,调用 XAsyncComplete。
- 提供一个围绕 XAsyncGetResult 的强类型包装器来返回结果。
- 如果你的异步调用没有数据负载,你应该提供一个围绕 XAsyncGetStatus 的强类型包装器,并将所需的缓冲区大小设为零传递给 XAsyncComplete。
- Begin 在 XAsyncBegin 期间以此操作码调用异步提供程序。如果提供程序实现了此操作码,则应通过调用 XAsyncSchedule 或通过外部方式启动其异步任务。此回调在 XAsyncBegin 调用链中同步调用,因此永远不应阻塞。
- DoWork 在通过任务队列调用 XAsyncSchedule 来调度异步工作的情况下调用。提供程序函数执行其需要的任何工作。完成时,它会以结果代码和数据负载大小调用 XAsyncComplete,如果调用没有数据负载,则大小可以为零。如果需要执行更多异步工作,提供程序可以调度该工作并应返回 E_PENDING。
- GetResult 被调用以获取调用的结果。因为数据大小在调用完成期间传递给 XAsyncComplete,所以此处不需要参数检查——所有缓冲区和缓冲区大小都已由库验证。
- Cancel 当用户取消异步调用时调用。如果调用可以取消,就取消它并以 E_ABORT 作为结果代码调用 XAsyncComplete。
- Cleanup 当调用完全结束时调用,提供程序可以删除任何动态内存。
