> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/async-patterns.md).

# 异步模式

使用 async/await、协程、链式调用、进度跟踪、取消和流来消费 IConvaiOperation\<T> 和 IConvaiStream\<T>。

`IConvaiOperation<T>` 和 `IConvaiStream<T>` 支持多种消费模式，因此你可以使用最适合你代码库的风格——纯 async/await、Unity 协程，或两者混合使用。有关类型定义和成员引用，请参阅 [操作与流类型](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/operation-and-stream-types.md).

***

### 异步/await

最直接的模式。适用于任何 `async` 方法。出错的操作会抛出 `ConvaiOperationException`.

```csharp
using Convai.Runtime.Core.Async;
using Convai.Runtime.Facades;
using System.Threading;
using UnityEngine;

public class AsyncConnectExample : MonoBehaviour
{
    private async void Start()
    {
        var manager = ConvaiManager.ActiveManager;
        if (manager == null) return;

        try
        {
            var session = await manager.ConnectAsync(destroyCancellationToken);
            Debug.Log($"已连接。房间：{session.RoomId}");
        }
        catch (ConvaiOperationException ex)
        {
            Debug.LogError($"[{ex.Code}] 连接失败：{ex.Message}");
        }
        catch (OperationCanceledException)
        {
            Debug.Log("连接已取消——场景已卸载。");
        }
    }
}
```

#### `await operation` 与 `await operation.AsTask()`

两者结果相同。优先使用 `await operation` 直接使用——它会用到 `GetAwaiter()` ，并避免额外分配。仅在需要将操作传递给要求 `AsTask()` 的方法时才使用 `Task<T>`，例如 `Task.WhenAll`.

```csharp
// 直接 await——推荐
var session = await manager.ConnectAsync();

// AsTask——仅在需要时
await Task.WhenAll(
    manager.ConnectAsync().AsTask(),
    someOtherTask
);
```

***

### 协程

当你的脚本不能使用 `async` （例如，在不支持 async 的 Unity 事件回调中），或者你更喜欢基于回调的流程时，请使用协程。

```csharp
using Convai.Runtime.Core.Async;
using Convai.Runtime.Facades;
using System.Collections;
using UnityEngine;

public class CoroutineConnectExample : MonoBehaviour
{
    private void Start()
    {
        var op = ConvaiManager.ActiveManager.ConnectAsync();
        StartCoroutine(op.ToCoroutine(
            onSuccess: session => Debug.Log($"已连接：{session.RoomId}"),
            onError:   err     => Debug.LogError($"[{err.Code}] {err.Message}")
        ));
    }
}
```

`ToCoroutine` 会一直挂起，直到操作完成。两者 `onSuccess` 和 `onError` 都是可选的——如果不需要回调，对任意一项都传 `null` 即可。

{% hint style="warning" %}
在协程中，出错的操作不会 **不** 抛出异常。如果你省略 `onError` 回调，失败将保持静默。

```csharp
// 错误——静默失败
yield return op.ToCoroutine(onSuccess: result => Use(result));

// 正确
yield return op.ToCoroutine(
    onSuccess: result => Use(result),
    onError:   err    => Debug.LogError(err.Message));
```

{% endhint %}

***

### ContinueWith 链式调用

将一个操作的结果转换为另一个操作，而无需嵌套 `await` 调用。

```csharp
// 同步转换
IConvaiOperation<string> roomIdOp = manager
    .ConnectAsync()
    .ContinueWith(session => session.RoomId);

string roomId = await roomIdOp;

// 异步转换——选择器返回 Task<TNext>
IConvaiOperation<string> nameOp = manager
    .ConnectAsync()
    .ContinueWith(async session =>
    {
        await SomeAsyncLookup(session.RoomId);
        return session.RoomId;
    });
```

`ContinueWith` 会传播错误：如果源操作出错，链式操作也会以相同错误出错，而不会调用选择器。

***

### 进度跟踪

轮询 `operation.Progress` 来驱动 UI 进度指示器。该值会从 `0.0` 为 `1.0` 随着操作完成而前进。并非所有操作都会报告细粒度进度——请检查 `状态` 以确定最终完成状态。

```csharp
using Convai.Runtime.Facades;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class ConnectProgressBar : MonoBehaviour
{
    [SerializeField] private Slider _progressBar;

    private IEnumerator ConnectWithProgress()
    {
        var op = ConvaiManager.ActiveManager.ConnectAsync();

        while (!op.IsCompleted)
        {
            _progressBar.value = op.Progress;
            yield return null;
        }

        _progressBar.value = 1f;

        if (op.HasError)
            Debug.LogError($"连接失败：{op.Error.Message}");
    }
}
```

***

### 取消

#### 使用 `CancellationToken`

将一个 `CancellationToken` 传递给任何 SDK 方法。当令牌被触发时，操作会转变为 `已取消` 状态。

```csharp
private CancellationTokenSource _cts;

private async void OnEnable()
{
    _cts = new CancellationTokenSource();

    try
    {
        await ConvaiManager.ActiveManager.ConnectAsync(_cts.Token);
    }
    catch (OperationCanceledException) { /* 预期 */ }
}

private void OnDisable()
{
    _cts?.Cancel();
    _cts?.Dispose();
}
```

#### 使用 `destroyCancellationToken`

`MonoBehaviour.destroyCancellationToken` 是作用于组件生命周期内操作的最简单取消模式——当组件被销毁时，令牌会自动被触发。

```csharp
private async void Start()
{
    await ConvaiManager.ActiveManager.ConnectAsync(destroyCancellationToken);
}
```

#### 使用 `operation.Cancel()`

调用 `Cancel()` 在操作句柄上直接调用，用于独立于 `CancellationToken`.

```csharp
var op = manager.ConnectAsync();

// 之后——取消操作
op.Cancel();
```

#### `CancellationToken` 与 `operation.Cancel()`

|           | `CancellationToken`           | `operation.Cancel()` |
| --------- | ----------------------------- | -------------------- |
| **来源**    | 外部（`CancellationTokenSource`) | 操作句柄                 |
| **使用场景**  | 组件生命周期、超时、关联取消                | 通过按钮或事件取消单个操作        |
| **与协程兼容** | 在创建操作时传递                      | 随时在句柄上调用             |

***

### 流

使用 `IConvaiStream<T>` 使用 `await foreach` 和 `await using` 用于资源清理。

```csharp
await using var stream = GetTokenStream();

await foreach (var token in stream.ReadAllAsync(destroyCancellationToken))
{
    AppendToTranscriptUI(token);
}
```

`ReadAllAsync` 返回 `IAsyncEnumerable<T>`。当流到达 `已完成`, `或`，或 `已取消`已故障 `await using` 时循环退出。务必包装在 `DisposeAsync()` 中，即使循环提前退出也会调用它。

{% hint style="danger" %}
切勿使用 `.Result` 阻塞 Unity 主线程——这会导致死锁。

```csharp
// 错误——会在主线程上死锁
var session = manager.ConnectAsync().AsTask().Result;
```

使用 `await` 或 `ToCoroutine()` 替代。
{% endhint %}

始终释放流。未能 `await using` 释放流会导致底层资源泄漏。

```csharp
// 错误——未释放
var stream = GetTokenStream();
await foreach (var item in stream.ReadAllAsync()) { ... }

// 正确
await using var stream = GetTokenStream();
await foreach (var item in stream.ReadAllAsync()) { ... }
```

***

### 错误处理决策表

| 场景                            | 模式                              | 原因                       |
| ----------------------------- | ------------------------------- | ------------------------ |
| `async void` MonoBehaviour 方法 | 使用 try/catch 的 Async/await      | 自然契合；故障会以异常形式显现          |
| UI 按钮回调（不支持 async）            | 带有 `onError` 回调的协程              | 按钮回调是同步的                 |
| 带转换的顺序操作                      | ContinueWith 链式调用               | 避免嵌套 await；自动传播错误        |
| 进度条或加载遮罩层                     | 协程轮询 `进度`                       | `yield return null` 每帧循环 |
| 组件生命周期作用域                     | `destroyCancellationToken`      | 零样板代码；自动清理               |
| 用户触发的取消（按钮）                   | `operation.Cancel()`            | 无需 CTS 的直接句柄控制           |
| 持续数据流                         | `await foreach` + `await using` | IAsyncEnumerable + 保证释放  |

***

### 使用示例

#### 示例 1——带进度条和取消按钮的加载遮罩层

一个医学培训模拟在会话连接时显示加载遮罩层，并提供可视化进度条和取消按钮，供希望在会话开始前退出的学习者使用。

{% code title="SessionLoadingOverlay.cs" %}

```csharp
using Convai.Runtime.Core.Async;
using Convai.Runtime.Facades;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class SessionLoadingOverlay : MonoBehaviour
{
    [SerializeField] private GameObject _overlayRoot;
    [SerializeField] private Slider     _progressBar;
    [SerializeField] private TMP_Text   _statusLabel;
    [SerializeField] private Button     _cancelButton;

    private IConvaiOperation<RoomSession> _connectOp;

    public void StartConnect()
    {
        _overlayRoot.SetActive(true);
        _connectOp = ConvaiManager.ActiveManager.ConnectAsync(destroyCancellationToken);
        _cancelButton.onClick.AddListener(OnCancelClicked);
        StartCoroutine(TrackConnect());
    }

    private IEnumerator TrackConnect()
    {
        _statusLabel.text = "正在连接…";

        while (!_connectOp.IsCompleted)
        {
            _progressBar.value = _connectOp.Progress;
            yield return null;
        }

        _overlayRoot.SetActive(false);
        _cancelButton.onClick.RemoveListener(OnCancelClicked);

        if (_connectOp.HasError)
            _statusLabel.text = $"失败：{_connectOp.Error.Message}";
        else if (!_connectOp.IsCanceled)
            _statusLabel.text = "已连接。";
    }

    private void OnCancelClicked() => _connectOp?.Cancel();
}
```

{% endcode %}

#### 示例 2——将流式转写令牌输出到自定义日志控件

一个企业入职模拟从 Convai 流式传输单个转写令牌，并将它们一次一个地追加到自定义日志控件中，产生打字机式效果。

{% code title="StreamingTranscriptLog.cs" %}

```csharp
using Convai.Runtime.Facades;
using TMPro;
using UnityEngine;

public class StreamingTranscriptLog : MonoBehaviour
{
    [SerializeField] private TMP_Text _log;

    // 在新的角色轮次开始时调用此方法
    public async void AttachToStream(IConvaiStream<string> tokenStream)
    {
        try
        {
            await using (tokenStream)
            {
                await foreach (var token in tokenStream.ReadAllAsync(destroyCancellationToken))
                {
                    _log.text += token;
                }
            }
        }
        catch (System.OperationCanceledException)
        {
            // 组件在流传输过程中被销毁——预期情况
        }
    }
}
```

{% endcode %}

***

### 故障排除

| 症状                                                    | 可能原因                                                              | 修复方法                                                                                           |
| ----------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| 操作保持在 `运行中` 无限期                                       | 等待从未到来的响应的 SDK 方法（网络超时）                                           | 设置一个 `CancellationToken` ，并设定超时： `new CancellationTokenSource(TimeSpan.FromSeconds(15)).Token` |
| `HasError` 是 `true` 但 `ConvaiOperationException` 不会抛出 | 使用协程路径——错误会传递给 `onError` 回调，而不会抛出                                 | 添加一个 `onError` 回调到 `ToCoroutine()`                                                             |
| `catch (ConvaiOperationException)` 在取消时永远不会命中         | 取消会抛出 `OperationCanceledException`，而不是 `ConvaiOperationException` | 添加一个单独的 `catch (OperationCanceledException)` 块                                                 |
| 一旦 `operation.Cancel()`                               | 在调用取消之前，操作已完成                                                     | 检查 `IsCompleted` 在调用之前 `Cancel()`                                                              |
| 场景卸载时流卡住                                              | `ReadAllAsync` 未传递 `CancellationToken`                            | 传递 `destroyCancellationToken` 为 `ReadAllAsync`                                                 |
| `ContinueWith` 选择器从不运行                                | 源操作出错；错误已传播，选择器被跳过                                                | 检查链式操作的 `HasError` 或捕获 `ConvaiOperationException` 在 await 处                                    |

***

### 下一步

有关这些模式背后的完整类型参考，请参阅 [操作与流类型](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/operation-and-stream-types.md)。对于返回 `IConvaiOperation<T>`的 SDK 方法，请参阅 [ConvaiManager API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/convaimanager-api.md) 和 [角色与玩家 API](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/character-and-player-api.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/scripting-reference/async-patterns.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
