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处理。