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天训练营课程