> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 异步编程模型

> 异步编程模型

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

<a id="gdk_asynchronous_model" />

## 概念模型

Microsoft Game Development Kit (GDK) 中的异步编程分为两个主要组件：任务（Task）和任务队列（Task Queue）。虽然库中还有更多功能，但整个概念模型都围绕这两个主要组件展开。

任务是一组可以启动、检查状态、可能取消、完成并返回其完成信息的异步工作。对于 Microsoft Game Development Kit (GDK) 模型，任务由两部分组成：工作回调和完成回调。这允许更强的控制能力，例如完全的并行处理，或者并行工作与单线程完成相结合。

任务队列是一个容器，用于将工作回调和完成回调入队以便稍后执行。任务队列中有两个内部队列，称为端口，分别处理工作回调和完成回调。它们被称为工作端口和完成端口。

**图 1. 任务和任务队列示意图**

<img src="https://mintcdn.com/microsoft-4404708b/sIrPFR_ir_sNKVEv/images/gdk/features/common/async_task_taskqueue_diagram.png?fit=max&auto=format&n=sIrPFR_ir_sNKVEv&q=85&s=89181466bdb096dbacd570ff0f1dcac5" alt="任务和任务队列示意图。" width="660" height="314" data-path="images/gdk/features/common/async_task_taskqueue_diagram.png" />

任务队列的每个端口都是[在创建时](/reference/system/xtaskqueue/functions/xtaskqueuecreate)不同地配置的，以创建不同的回调执行行为。例如，工作端口可以配置为异步执行，而完成端口可以配置为在主线程上串行运行。可以设置手动模式以完全控制执行行为。端口配置模式在[下方进行了说明](#controlling_work_dispatching)。

启动异步任务时，回调不会立即入队到任务队列。[异步提供程序](/build/core-features/common/async/async-libraries/async-library-xasyncprovider)负责处理状态变化，以确保在完成回调入队并分派之前，工作先被入队并分派。

任务队列本身不直接处理线程。相反，它依赖外部调用来[分派](/reference/system/xtaskqueue/functions/xtaskqueuedispatch)其端口。外部调用决定了线程和并发行为。任务队列本身是完全线程安全的。

**图 2. 端口被分派到多个线程**

<img src="https://mintcdn.com/microsoft-4404708b/sIrPFR_ir_sNKVEv/images/gdk/features/common/async_port_dispatch_multithread.png?fit=max&auto=format&n=sIrPFR_ir_sNKVEv&q=85&s=697aae48e97cb3aa23845108060a1882" alt="显示端口被分派到多个线程的图片。" width="628" height="230" data-path="images/gdk/features/common/async_port_dispatch_multithread.png" />

基本上就是这样！任务的回调被入队到任务队列的工作端口和完成端口，然后任务队列以某种方式分派这些回调。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](/reference/system/xasync/structs/xasyncblock)：

```c++ theme={null}
typedef void CALLBACK XAsyncCompletionRoutine(struct XAsyncBlock* asyncBlock);

struct XAsyncBlock
{
    XTaskQueueHandle queue;
    void* context;
    XAsyncCompletionRoutine* callback;
    unsigned char internal[sizeof(void*) * 4];
};
```

[XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 是一个由调用方提供的结构体。调用方在此结构中填写可选字段，如下表所示。

| 字段           | 描述                                                                                           |
| ------------ | -------------------------------------------------------------------------------------------- |
| **queue**    | 一个任务队列句柄，可控制哪些线程执行异步调用。如果此参数为 null，则使用进程任务队列。如果进程任务队列已设置为 null，则调用会失败并返回 E\_NO\_TASK\_QUEUE。 |
| **context**  | 传递给回调函数的可选上下文指针。                                                                             |
| **callback** | 在操作完成时调用的可选回调函数。                                                                             |

**Internal** 字段由系统使用，不应被修改。此结构中用户可设置的字段在异步操作期间不应被修改。[XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 必须在异步操作的生命周期内保留在内存中。如果 [XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 是动态分配的，则完成回调是最早可以删除它的时机。

除了 [XAsyncBlock](/reference/system/xasync/structs/xasyncblock)，还有少量辅助 API，如下所示。

```c++ theme={null}
STDAPI XAsyncGetStatus(XAsyncBlock* asyncBlock, bool wait);

STDAPI XAsyncGetResultSize(XAsyncBlock* asyncBlock, size_t* bufferSize);

STDAPI_(void) XAsyncCancel(XAsyncBlock* asyncBlock);

typedef HRESULT CALLBACK XAsyncWork(XAsyncBlock* asyncBlock);

STDAPI XAsyncRun(XAsyncBlock* asyncBlock, XAsyncWork* work);
```

[XAsyncGetStatus](/reference/system/xasync/functions/xasyncgetstatus) 返回异步调用的状态。当调用开始时，此状态为 E\_PENDING。完成时会变为 S\_OK 或特定错误。如果调用被取消，则返回 E\_ABORT。

[XAsyncGetResultSize](/reference/system/xasync/functions/xasyncgetresultsize) 返回获取调用结果所需的缓冲区大小。实际获取结果的 API 针对每个异步调用进行了定制。

[XAsyncCancel](/reference/system/xasync/functions/xasynccancel) 可用于取消调用。取消由被取消的操作决定，可能是同步的、异步的，或根本不发生。如果操作被取消，则 [XAsyncGetResult](/reference/system/xasyncprovider/functions/xasyncgetresult)、[XAsyncGetResultSize](/reference/system/xasync/functions/xasyncgetresultsize) 或 [XAsyncGetStatus](/reference/system/xasync/functions/xasyncgetstatus) 会返回 E\_ABORT。已取消的调用会向 [XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 的 [XAsyncCompletionRoutine](/reference/system/xasync/functions/xasynccompletionroutine) 参数发出信号并调用其回调。

[XAsyncRun](/reference/system/xasync/functions/xasyncrun) 是一个辅助方法，可以异步运行任何代码。

### 异步 API 使用

首先，让我们看看以下代码示例中的同步 API。

```c++ theme={null}
HRESULT XGameSaveGetRemainingQuota(XGameSaveProviderHandle provider,
int64_t* remainingQuota);
```

此 API 调用 Web 服务以确定还剩多少存档存储空间。要添加异步支持，我们声明一对新 API。

```c++ theme={null}
HRESULT XGameSaveGetRemainingQuotaAsync(XGameSaveProviderHandle
provider, XAsyncBlock* async);

HRESULT XGameSaveGetRemainingQuotaResult(XAsyncBlock* async,
int64_t* remainingQuota);
```

如果异步调用已启动，[XGameSaveGetRemainingQuotaAsync](/reference/system/xgamesave/functions/xgamesavegetremainingquotaasync) 会返回 S\_OK（因为此 API 只有异步版本，返回 E\_PENDING 没有意义）。在调用完成之前，[XGameSaveGetRemainingQuotaResult](/reference/system/xgamesave/functions/xgamesavegetremainingquotaresult) 返回 E\_PENDING。

让我们看看下面的实际使用情况。

```c++ theme={null}
// providerHandle is a previously obtained XGameSaveProviderHandle.

XAsyncBlock* b = new XAsyncBlock;
ZeroMemory(b, sizeof(XAsyncBlock));
b->context = this;
b->queue = queue;
b->callback = [](XAsyncBlock* async)
{
    int64_t remainingQuota;
    if(SUCCEEDED(XGameSaveGetRemainingQuotaResult(async, &remainingQuota)))
    {
        printf("Remaining quota: %irn", remainingQuota);
    }
    delete async;
};
XGameSaveGetRemainingQuotaAsync(providerHandle, b);
```

[XAsyncBlocks](/reference/system/xasync/structs/xasyncblock) 全都需要一个任务队列（如下所述），任务队列控制异步调用的执行位置和方式。如果未提供任务队列，则使用进程范围的任务队列。

请注意，[XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 需要在异步调用的生命周期内保留在内存中。在此示例中，它是动态分配的，并在完成回调中删除。它也可以存储为全局变量或成员变量。如果同一个 [XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 同时被用于多个异步调用，则行为未定义。

[XGameSaveGetRemainingQuotaResult](/reference/system/xgamesave/functions/xgamesavegetremainingquotaresult) 完成了异步调用的整个循环。它释放异步块中的内部数据，因此该块现在可以用于新的调用。对 [XGameSaveGetRemainingQuotaResult](/reference/system/xgamesave/functions/xgamesavegetremainingquotaresult) 的后续调用将失败。[XGameSaveGetRemainingQuotaAsync](/reference/system/xgamesave/functions/xgamesavegetremainingquotaasync) 和 [XGameSaveGetRemainingQuotaResult](/reference/system/xgamesave/functions/xgamesavegetremainingquotaresult) 也在异步块内成对使用——如果你将一个异步调用与另一个结果 API 不匹配地混用，就会发生错误。

如果异步调用没有数据负载，意味着只有 HRESULT 状态很重要，那么定义一个只接受异步块的 **Result** 方法，如下所示。

```c++ theme={null}
HRESULT QueryUpdateStatusAsyncResult(_Inout_ XAsyncBlock* block);
```

<a id="controlling_work_dispatching" />

### 控制工作分派

前面调用中的异步工作是在哪个线程上完成的？完成回调是哪个线程调用的？这是由分配给 [XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 的任务队列决定的。

任务队列有两个“端口”：*工作端口*和*完成端口*。每个端口都有一个分派模式，用于决定入队到该端口的回调如何处理。有几种分派模式。

* **线程池：** 入队到线程池队列的回调在系统线程池上执行。线程池会并行调用这些回调，当线程池线程可用时依次从队列中取出一个调用来执行。

* **串行线程池：** 回调入队并在线程池上运行，但每次运行一个。

* **手动：** 入队到手动队列的回调不会自动分派。开发者可以自行选择在任何想要的线程上分派它们。

* **立即：** 立即分派模式根本不入队。它会立即在提交回调的线程上执行调用。

存在一个默认的进程任务队列，其工作端口和完成端口都通过系统线程池进行分派。如果 [XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 中未传入任何队列参数，就会使用此进程任务队列。游戏也可以禁用进程任务队列，要求向 [XAsyncBlock](/reference/system/xasync/structs/xasyncblock) 传入一个队列。

我们预期许多开发者会选择手动分派模式，以完全控制异步工作和完成回调的执行时机和位置。

有关任务队列的详细信息，请参阅[异步任务队列设计](/build/core-features/common/async/async-task-queue-design)。

## 通知

通知可能没有结束，并且可能被调用多次。通知应支持异步调用要求的一个子集。

1. 带轮询的异步
2. 带回调的异步
3. 控制回调发生的线程

通知使用任务队列以便开发者控制回调线程，但除此之外不使用异步块——它们的设计更像是带有 **Register** 和 **Unregister** 方法的标准事件。

* 一个 **Register** 方法，它接受任何调用特定的参数、一个任务队列、一个可选的 void 上下文以及一个强类型的回调指针。最后一个参数是一个*输出*参数，用于返回一个令牌。

* 一个 **Unregister** 方法，它接受任何调用特定的上下文和令牌。

* 通过添加一个与通知回调无关的单独方法来支持轮询。

让我们看看下面这个可能获取 Windows 消息的示例。

```c++ theme={null}
struct XTaskQueueRegistrationToken;

typedef void MessageAvailableCallback(void* context, const MSG* msg);

HRESULT RegisterMessageAvailable(
    XTaskQueueHandle queue,
    void* context,
    MessageAvailableCallback* callback,
    XTaskQueueRegistrationToken * token);

bool UnregisterMessageAvailable(XTaskQueueRegistrationToken token, bool
wait);

// Usage.
XTaskQueueRegistrationToken token;
RegisterMessageAvailable(queue, nullptr, [](void*, const MSG* msg)
{
    printf("Message: %drn", msg->message);
}, &token);
```

请注意，在此示例中，**UnregisterMessageAvailable** 接受最后一个 “wait” 参数并返回一个 bool。这允许调用方决定在调用被触发时如何处理注销。

<a id="heading-7" />

## 异步库

为了更容易创建支持异步模式的一致 API，我们提供了一个库，可用于实现 API 的“异步基础设施”。该库的 API 如下所示。

```c++ theme={null}
enum class XAsyncOp : uint32_t
{
    Begin,
    DoWork,
    GetResult,
    Cancel,
    Cleanup
};

struct XAsyncProviderData
{
    XAsyncBlock* async;  
    size_t bufferSize;  
    void* buffer;  
    void* context;
};

typedef HRESULT CALLBACK XAsyncProvider(
_In_ XAsyncOp op,
_Inout_ XAsyncProviderData* data);

STDAPI XAsyncBegin (
_Inout_ XAsyncBlock* asyncBlock,
_In_opt_ void* context,
_In_opt_ void* identity,
_In_opt_ const char* identityName,
_In_ XAsyncProvider* provider);

STDAPI XAsyncSchedule(
_Inout_ XAsyncBlock* asyncBlock,
_In_ uint32_t delayInMs);

STDAPI_(void) XAsyncComplete(
_Inout_ XAsyncBlock* asyncBlock,
_In_ HRESULT result,
_In_ size_t requiredBufferSize);

STDAPI XAsyncGetResult(
_Inout_ XAsyncBlock* asyncBlock,
_In_opt_ void* identity,
_In_ size_t bufferSize,
_Out_writes_bytes_opt_(bufferSize) void* buffer,
_Out_opt_ size_t* bufferUsed);
```

此 API 使用单个回调，并结合一个操作值来指示调用 API 的原因。还有一个单独的数据结构，在调用推进过程中会被填充。要使用此 API，请执行以下操作。

1. 使用调用方传入的异步块调用 [XAsyncBegin](/reference/system/xasyncprovider/functions/xasyncbegin)，并提供一个包含实现的回调。

2. 为该调用执行异步工作。如果你需要在工作线程上运行工作，请调用 [XAsyncSchedule](/reference/system/xasyncprovider/functions/xasyncschedule)。如果你可以使用操作系统异步原语来执行工作，并且能够足够快地设置这些原语以保持时间关键安全，那更好。

3. 如果你需要从工作线程回调中调用其他异步工作，可以从工作线程返回 E\_PENDING。你也可以在工作线程内部调用 [XAsyncSchedule](/reference/system/xasyncprovider/functions/xasyncschedule) 来重新调度更多工作。

4. 当所有工作完成时，调用 [XAsyncComplete](/reference/system/xasyncprovider/functions/xasynccomplete)。

5. 提供一个围绕 [XAsyncGetResult](/reference/system/xasyncprovider/functions/xasyncgetresult) 的强类型包装器来返回结果。

6. 如果你的异步调用没有数据负载，你应该提供一个围绕 [XAsyncGetStatus](/reference/system/xasync/functions/xasyncgetstatus) 的强类型包装器，并将所需的缓冲区大小设为零传递给 [XAsyncComplete](/reference/system/xasyncprovider/functions/xasynccomplete)。

异步提供程序回调会通过以下操作调用。

* **Begin** 在 [XAsyncBegin](/reference/system/xasyncprovider/functions/xasyncbegin) 期间以此操作码调用异步提供程序。如果提供程序实现了此操作码，则应通过调用 [XAsyncSchedule](/reference/system/xasyncprovider/functions/xasyncschedule) 或通过外部方式启动其异步任务。此回调在 [XAsyncBegin](/reference/system/xasyncprovider/functions/xasyncbegin) 调用链中同步调用，因此永远不应阻塞。

* **DoWork** 在通过任务队列调用 [XAsyncSchedule](/reference/system/xasyncprovider/functions/xasyncschedule) 来调度异步工作的情况下调用。提供程序函数执行其需要的任何工作。完成时，它会以结果代码和数据负载大小调用 [XAsyncComplete](/reference/system/xasyncprovider/functions/xasynccomplete)，如果调用没有数据负载，则大小可以为零。如果需要执行更多异步工作，提供程序可以调度该工作并应返回 E\_PENDING。

* **GetResult** 被调用以获取调用的结果。因为数据大小在调用完成期间传递给 [XAsyncComplete](/reference/system/xasyncprovider/functions/xasynccomplete)，所以此处不需要参数检查——所有缓冲区和缓冲区大小都已由库验证。

* **Cancel** 当用户取消异步调用时调用。如果调用可以取消，就取消它并以 E\_ABORT 作为结果代码调用 [XAsyncComplete](/reference/system/xasyncprovider/functions/xasynccomplete)。

* **Cleanup** 当调用完全结束时调用，提供程序可以删除任何动态内存。

异步提供程序只需要实现它所需的操作。例如，没有清理需求的不可取消异步 IO 只需要实现 **GetResult**。

以下是一个 **FactorialAsync** 方法的示例，它异步实现阶乘。

```c++ theme={null}
UINT64 Factorial(UINT64 value)
{
    UINT64 result = 1;

    while (value != 0)
    {       
        result *= value;
        value--;
    }

    return result;
}

HRESULT FactorialAsync(UINT64 value, XAsyncBlock* async)
{
    struct CallData
    {
        UINT64 value;
        UINT64 result;
    };

    CallData* data = new CallData();
    data->value = value;
    data->result = 1;

    HRESULT hr = XAsyncBegin (async, data, FactorialAsync, __FUNCTION__, []
        (XAsyncOp op, XAsyncProviderData* data)
    {
        CallData* d = (CallData*)data->context;

        switch (op)
        {
        case XAsyncOp::Begin:
            return XAsyncSchedule(data->async, 0);

        case XAsyncOp::Cleanup:
            delete d;
            break;

        case XAsyncOp::GetResult:
            CopyMemory(data->buffer, &d->result, sizeof(UINT64));
            break;
 
        case XAsyncOp::DoWork:
            data->result = Factorial(data.Value);
            XAsyncComplete(data->async, S_OK, sizeof(UINT64));
            break;
        }

        return S_OK;
    });

    return hr;
}

HRESULT FactorialAsyncResult(XAsyncBlock* async, UINT64* result)
{
    return XAsyncGetResult(async, FactorialAsync, sizeof(UINT64), result);
}
```

## 参考 API 文档

* [XAsync (API 内容)](/reference/system/xasync/xasync_members)
  * 函数
    * [XAsyncGetStatus](/reference/system/xasync/functions/xasyncgetstatus)
    * [XAsyncGetResultSize](/reference/system/xasync/functions/xasyncgetresultsize)
    * [XAsyncCancel](/reference/system/xasync/functions/xasynccancel)
    * [XAsyncCompletionRoutine](/reference/system/xasync/functions/xasynccompletionroutine)
    * [XAsyncRun](/reference/system/xasync/functions/xasyncrun)
  * 结构体
    * [XAsyncBlock](/reference/system/xasync/structs/xasyncblock)
* [XAsyncProvider (API 内容)](/reference/system/xasyncprovider/xasyncprovider_members)
  * 函数
    * [XAsyncGetResult](/reference/system/xasyncprovider/functions/xasyncgetresult)
    * [XAsyncBegin](/reference/system/xasyncprovider/functions/xasyncbegin)
    * [XAsyncSchedule](/reference/system/xasyncprovider/functions/xasyncschedule)
    * [XAsyncComplete](/reference/system/xasyncprovider/functions/xasynccomplete)
* [XTaskQueue (API 内容)](/reference/system/xtaskqueue/xtaskqueue_members)
  * 函数
    * [xtaskqueuecreate](/reference/system/xtaskqueue/functions/xtaskqueuecreate)
    * [xtaskqueuedispatch](/reference/system/xtaskqueue/functions/xtaskqueuedispatch)
* [xgamesave (API 内容)](/reference/system/xgamesave/xgamesave_members)
  * 函数
    * [XGameSaveGetRemainingQuotaAsync](/reference/system/xgamesave/functions/xgamesavegetremainingquotaasync)
    * [XGameSaveGetRemainingQuotaResult](/reference/system/xgamesave/functions/xgamesavegetremainingquotaresult)

## 另请参阅

[异步编程设计目标和改进](/build/core-features/common/async/async-whitepaper)
[异步任务队列设计](/build/core-features/common/async/async-task-queue-design)


## Related topics

- [常见 GDK 功能](/zh-CN/build/core-features/common/common-features-overview.md)
- [面向 GDK 的 Unity C# API 包装器](/zh-CN/build/gdk-and-engines/unity/unity-api-wrappers.md)
- [XBOX GDK 中的异步编程模型](/zh-CN/build/core-features/common/async/index.md)
- [异步编程概述](/zh-CN/build/core-features/common/async/async-toc.md)
- [XAsync](/zh-CN/reference/system/xasync/xasync_members.md)
