Async/Await是啥就不介绍了,类似协程但是更简洁,这里默认你有现代异步编程的知识。
总结下Unity内部实现,方便搞清楚到底干了些什么。简单说就是,Unity内部做了一些处理,保证了2点,调用方是主线程时:1.调用方不会发生线程切换,2.被调用的异步函数不会执行在其他线程。
Unity对Async模式的目标
Async模式更现代化,是未来方向,但Unity目前并不打算用Async替代协程(IEnumerators),主要目的是用在IO类等操作上。
Unity Async性能比协程高,但并行数较低
适合单件/主体类对象使用,比如摄像机,界面,IO方法等。少量Async性能高于协程,对于大量Object都需要并行运行的代码,仍然应该使用协程。
性能部分
尽量返回Unity包装的Awaitable
,而不是Task
(Unity6)
.Net的Async是用Task
来做,如下:
async Task function() {
await Task.Delay(1000);
}
.Net原生的Task
每次返回都是新建对象,Unity包装的Awaitable
对象是共用的,因此GC压力比较小。Awaitable
还有线程切换“免费”(无损耗)的优势,见下一节。
async Awaitable function() {
while(true) {
await Awaitable.NextFrameAsync();
}
}
对于需要反复await
的方法,类似上述示例代码,请使用Awaitable
返回值。当然IO那种一次性await
的方法无所谓。
Awaitable
对象因为是共用的(await后就给别人用了),不能重复await(如下述代码),会造成死锁。
async Awaitable function() {
while(true) {
var returned_awaitable = Awaitable.NextFrameAsync();
await returned_awaitable;
await returned_awaitable; //deadlock
}
}
Unity内部管理Async的上下文(切换),和.Net不同
Async的主要概念,恢复Resume
,指的是切换回来,继续执行await
后的代码。
“上下文”决定如何恢复await
的代码,.Net会用线程池在不同异步代码中切换。比如以下.Net代码:
using System;
async Task function() {
Console.WriteLine($"task start: Tid {Environment.CurrentManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"task end: Tid {Environment.CurrentManagedThreadId}");
}
Console.WriteLine($"caller start, Tid{Environment.CurrentManagedThreadId}");
await function();
Console.WriteLine($"caller end, Tid{Environment.CurrentManagedThreadId}");
执行结果是:
caller start, Tid12
task start: Tid 12
task end: Tid 38
caller end, Tid38
可以看到调用方和执行方都发生了线程切换。
而Unity为了保证主线程调用的方法只在主线程中执行,它们自己包装了个上下文方法叫UnitySynchronizationContext
(这是内部实现,我们不用管只要知道就行)。
所以不用担心线程问题,无论返回值是Awaitable
还是.Net的Task
,只要是主线程调用的,都会被UnitySynchronizationContext
管理并保证在主线程上执行。
但用Task
返回值的Async函数,在恢复Resume
时,只能在下一帧恢复而不是立即恢复,且Unity需要多执行一步替换.Net上下文的操作,因此Awaitable
性能更高。
当然在其他线程调用的Async函数无主线程限制需求,会依然使用.Net线程池。
用法模式
射后不管模式
Async函数调用时可以不用await
,系统内部将持续执行该Async函数。
async Awaitable worker() {
await Awaitable.WaitForSecondsAsync(5);
Debug.Log($"worker end, frame {Time.frameCount}");
}
void caller() {
_ = worker();
Debug.Log($"caller end, frame {Time.frameCount}");
}
结果:
caller end, frame 1
worker end, frame 292
caller
没有写await
,但worker
将依然执行,只是不再等待他完成。
如果是.Net中,则会在线程池中持续执行worker
,在Unity中则由主线程loop持续执行。
这种模式也常用于在普通Sync同步函数中,调用Async异步函数。
Unity用Async可以很方便的实现多线程编程
通过这个可以方便的实现多线程代码,而不需要Job之类。让任意代码片段执行在不同线程。
如果你希望切换到后台线程池(类似.Net线程池实现),或者切换到主线程,可以手动指定。比如:
async Awaitable function() {
await Awaitable.BackgroundThreadAsync();
...
return null;
}
这行await
后的代码,直到函数结束,都将执行在后台.Net托管线程池。
如果调用方是主线程,切换不会改变调用方的线程,非常方便。注意切换至少需要耗时1帧。
切换到主线程就是await Awaitable.MainThreadAsync();
,一般不需要调用。
但不幸的,在WebGL(wasm)下,你不能使用.Net多线程,包括任何Task
的方法,以及Awaitable.BackgroundThreadAsync()
。
Async函数可以定义为void,方便各种event
回调
如果想要回调执行你的Async函数,不用再套一层普通函数来调用你的异步函数,可以用async void
,类似如下:
async void function(CallbackContext ctx) {
try{
...do something...
await Awaitable.WaitForSecondsAsync(5);
}catch (Exception e){
Debug.LogError(e);
}
}
void Start() {
InputAction.performed += function;
}
注意async void
执行在最顶层,因此函数中的异常不会被外部发现,需要你自己catch处理。