> 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).

# 异步模式

`IConvaiOperation<T>` 和 `IConvaiStream<T>` 支持多种消费模式，因此你可以使用最适合你的代码库的风格——纯异步/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` 随着操作完成而推进。并非所有操作都会报告细粒度进度——请检查 `Status` 以确认最终完成。

```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 %}

{% hint style="warning" %}
始终释放流。未释放流会泄漏底层资源。 `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()) { ... }
```

{% endhint %}

***

### 错误处理决策表

| 场景                            | 模式                              | 原因                       |
| ----------------------------- | ------------------------------- | ------------------------ |
| `async void` MonoBehaviour 方法 | 使用 try/catch 的异步/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` 处捕获                                         |

***

### 下一步

有关这些模式背后的完整类型参考，请参见 [操作与流类型](/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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

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

The question should be specific, self-contained, and written in natural language.
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.
