小心!在Unity中这么用async和await有问题

Async/await 或 Task 异步编程 (TAP) 模型是一种非常优雅的异步编程解决方案。但是,很多复杂性都被隐藏起来了,这会导致意想不到的行为,尤其是在 Unity 中工作时。

例如,这段代码可以正常工作:

HttpClient client = new HttpClient();
var initContent = await client.GetAsync(initUri);

// 执行一些初始化操作
SceneManager.LoadScene(nextSceneIndex);

而这段代码肯定会抛出异常:

HttpClient client = new HttpClient();
await client.GetAsync(initUri).ContinueWith(initContentTask =>
{
    var initContent = initContentTask.Result;

    // 执行一些初始化操作

    SceneManager.LoadScene(nextSceneIndex);
});

但是对于没有经过培训的人来说,这两段代码看起来可能是一样的。所以让我们深入了解幕后发生了什么。

它在 C# 中是如何工作的

任务和 TaskScheduler

任务最初并不是为 TAP 模型而设计的。这意味着它们是在 async/await 生态系统之外设计的。它们是在之前创建的,并且作为通用异步工作的包装器。

任务还支持使用延续与其他任务进行链接,就像我们之前使用 Task.ContinueWith 所看到的那样。默认情况下,此方法可以在不同的线程上运行你传入的延续。这是问题的核心,因为大多数 Unity API 要求从主线程调用。

这是因为大多数 Task 的方法使用 TaskScheduler 来处理异步工作。而它们将使用的默认 TaskScheduler (TaskScheduler.Current) 可能会在不同的线程上运行工作。

Await 和 SynchronizationContext

Await 语句是“将我的方法的剩余部分放在一个 Action 中,并将此 Action 传递给任务”的语法糖。但是 Await 不使用 Task.ContinueWith。相反,“解包”后的代码看起来像这样:

HttpClient client = new HttpClient();
var __awaiter = client.GetAsync(initUri).GetAwaiter();
__awaiter.OnCompleted(() =>
{
    var initContent = __awaiter.GetResult(); 

    // 执行一些初始化操作

    SceneManager.LoadScene(nextSceneIndex);
});

它比这稍微复杂一些,因为更多上下文被传递到 OnCompleted 的委托中。像 using 或 try/catch 这样的块仍然适用于委托内部。这只是为了说明目的。

因此,await 使用 Task.GetAwaiter()**.**OnCompleted(Action)。主要区别在于“Awaiter”使用 SynchronizationContext.Current 来调度它们的异步工作,而不是 TaskScheduler.Current。它们都是静态属性,但 SynchronizationContext.Current 有一个很大的优势:它有一个 setter。这意味着代码的其他部分可以“覆盖”SynchronizationContext 的默认行为,就像它在普通 C# 中设计的那样。

这正是 Unity 中发生的事情。Unity(像其他框架一样)创建了自己的 SynchronizationContext,它在主线程中执行操作并将它们放入 SynchronizationContext.Current 中。这将使 await 之后的代码在主线程上运行。

我们看看异步函数的声明:

async void SyncFunctionTest()
{
}

很简洁明了,就一个async关键字。虽然被标记异步关键字,但是,还是在当前线程执行。最后我们打印下线程的ID,证明我之前的说法是正确的,代码是这样的:

void Start()
{
    Debug.Log(Thread.CurrentThread.ManagedThreadId);
    SyncFunctionTest();
}

async void SyncFunctionTest()
{
    Debug.Log(Thread.CurrentThread.ManagedThreadId);
}

我们看看控制台:

图片

 

你会看到,在async函数中执行的线程id仍然是1,说明异步函数并没有在新线程中执行。

总结

Task 异步编程 (TAP) 系统是一个强大的异步编程范式。但是,使用它会导致 Unity 中出现线程问题。

  • 在 Unity 中,从主线程调用的异步方法将在主线程上完全运行。拥有异步方法并不会使你的代码多线程化!

  • 在使用 TaskSchedulers 的方法时要小心。它们默认情况下不会使用 Unity 的同步上下文,并且不提供相同的保证(你可能会最终在其他线程上执行代码!)。

那async如何才能在新线程中执行呢?有很多种办法,可以手动修改SynchronizationContext,不过更简单的是使用UniTask。

想了解更多游戏开发知识,可以扫描下方二维码,免费领取游戏开发4天训练营课程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值