UniTask
为统一提供有效的无异步分配/等待集成。
- 基于struct的
UniTask <T>和自定义异步分组以实现零分配 - 使所有的统一异步和共同点等待
- 基于playerloop的任务(
UniTask .Yield,UniTask .Delay,UniTask .DelayFrame等),可以更换所有Coroutine操作 - Monobehaviour消息事件和UGUI事件(等待/异步)可以实现
- 完全在Unity的PlayerLoop上运行,因此不使用线程并在WebGL,Wasm等上运行。
- 异步LINQ,带有通道和异步性Property
- 任务跟踪器窗口以防止内存泄漏
- 高度兼容的行为与任务/valueTask/ivaluetasksource
有关技术详细信息,请参阅博客文章: UniTask V2 - 零分配异步/等待Unity,以及异步LINQ
有关高级提示,请参见博客文章:通过异步装饰器图案扩展Unitywebrequest - UniTask的高级技术
目录
- 入门
- UniTask和异步的基础知识
- 取消和例外处理
- 超时处理
- 进步
- PlayerLoop
- 异步void vs async UniTask void
- UniTask跟踪器
- 外部资产
- 异步和异步LINQ
- 等待事件
- 渠道
- VS等待
- 用于单元测试
- ThreadPool限制
- ienumerator.to UniTask限制
- 对于Unityeditor
- 与标准任务API进行比较
- 合并配置
- 分配分配器
- UniTask同步context
- API参考
- UPM软件包
- 通过git URL安装
- .NET核心
- 执照
入门
通过UPM软件包安装git参考或资产软件包( UniTask .*.*.*.unitypackage ),可在UniTask /发布中提供。
// extension awaiter/methods can be used by this namespace
using Cysharp . Threading . Tasks ;
// You can return type as struct UniTask <T>(or UniTask ), it is unity specialized lightweight alternative of Task<T>
// zero allocation and fast excution for zero overhead async/await integrate with Unity
async UniTask < string > DemoAsync ( )
{
// You can await Unity's AsyncObject
var asset = await Resources . LoadAsync < TextAsset > ( "foo" ) ;
var txt = ( await UnityWebRequest . Get ( "https://..." ) . SendWebRequest ( ) ) . downloadHandler . text ;
await SceneManager . LoadSceneAsync ( "scene2" ) ;
// .WithCancellation enables Cancel, GetCancellationTokenOnDestroy synchornizes with lifetime of GameObject
// after Unity 2022.2, you can use `destroyCancellationToken` in MonoBehaviour
var asset2 = await Resources . LoadAsync < TextAsset > ( "bar" ) . WithCancellation ( this . GetCancellationTokenOnDestroy ( ) ) ;
// .To UniTask accepts progress callback(and all options), Progress.Create is a lightweight alternative of IProgress<T>
var asset3 = await Resources . LoadAsync < TextAsset > ( "baz" ) . To UniTask ( Progress . Create < float > ( x => Debug . Log ( x ) ) ) ;
// await frame-based operation like a coroutine
await UniTask . DelayFrame ( 100 ) ;
// replacement of yield return new WaitForSeconds/WaitForSecondsRealtime
await UniTask . Delay ( TimeSpan . FromSeconds ( 10 ) , ignoreTimeScale : false ) ;
// yield any playerloop timing(PreUpdate, Update, LateUpdate, etc...)
await UniTask . Yield ( PlayerLoopTiming . PreLateUpdate ) ;
// replacement of yield return null
await UniTask . Yield ( ) ;
await UniTask . NextFrame ( ) ;
// replacement of WaitForEndOfFrame
#if UNITY_2023_1_OR_NEWER
await UniTask . WaitForEndOfFrame ( ) ;
#else
// requires MonoBehaviour(CoroutineRunner))
await UniTask . WaitForEndOfFrame ( this ) ; // this is MonoBehaviour
#endif
// replacement of yield return new WaitForFixedUpdate(same as UniTask .Yield(PlayerLoopTiming.FixedUpdate))
await UniTask . WaitForFixedUpdate ( ) ;
// replacement of yield return WaitUntil
await UniTask . WaitUntil ( ( ) => isActive == false ) ;
// special helper of WaitUntil
await UniTask . WaitUntilValueChanged ( this , x => x . isActive ) ;
// You can await IEnumerator coroutines
await FooCoroutineEnumerator ( ) ;
// You can await a standard task
await Task . Run ( ( ) => 100 ) ;
// Multithreading, run on ThreadPool under this code
await UniTask . SwitchToThreadPool ( ) ;
/* work on ThreadPool */
// return to MainThread(same as `ObserveOnMainThread` in UniRx)
await UniTask . SwitchToMainThread ( ) ;
// get async webrequest
async UniTask < string > GetTextAsync ( UnityWebRequest req )
{
var op = await req . SendWebRequest ( ) ;
return op . downloadHandler . text ;
}
var task1 = GetTextAsync ( UnityWebRequest . Get ( "http://goog*l*e*.com" ) ) ;
var task2 = GetTextAsync ( UnityWebRequest . Get ( "http://*bi*ng.co*m" ) ) ;
var task3 = GetTextAsync ( UnityWebRequest . Get ( "http://yahoo***.com" ) ) ;
// concurrent async-wait and get results easily by tuple syntax
var ( google , bing , yahoo ) = await UniTask . WhenAll ( task1 , task2 , task3 ) ;
// shorthand of WhenAll, tuple can await directly
var ( google2 , bing2 , yahoo2 ) = await ( task1 , task2 , task3 ) ;
// return async-value.(or you can use ` UniTask `(no result), ` UniTask Void`(fire and forget)).
return ( asset as TextAsset ) ? . text ?? throw new InvalidOperationException ( "Asset not found" ) ;
}
UniTask和异步的基础知识
UniTask功能依赖于C#7.0(类似任务的自定义ASYNC方法构建器功能),因此所需的Unity版本是在Unity 2018.3之后,支持的官方最低版本是Unity 2018.4.13f1 。
为什么需要使用UniTask (自定义任务样本对象)?因为任务太重,与Unity线程(单线程)不匹配。 UniTask不使用线程和同步context/executionContext,因为Unity的发动机层自动调度了Unity的异步对象。它实现了更快和降低的分配,并且与统一完全集成在一起。
您可以等待AsyncOperation , ResourceRequest , AssetBundleRequest , AssetBundleCreateRequest , UnityWebRequestAsyncOperation , AsyncGPUReadbackRequest ,iEnumerator,ienumerator, IEnumerator和其他using Cysharp.Threading.Tasks; 。
UniTask提供了三种扩展方法的模式。
* await asyncOperation ;
* . WithCancellation ( CancellationToken ) ;
* . To UniTask ( IProgress , PlayerLoopTiming , CancellationToken ) ; WithCancellation是一个简单的To UniTask的版本,均为返回UniTask 。有关取消的详细信息,请参见:取消和异常处理部分。
注意:等待直接从playerloop的本机时间返回,但与指定的playerlooptiming一起返回了cancellation和UniTask 。有关定时的详细信息,请参见:PlayerLoop部分。
注意:AssetBundLereQuest具有
asset和allAssets,默认值等待返回asset。如果要获得allAssets,则可以使用AwaitForAllAssets()方法。
UniTask的类型可以使用UniTask .WhenAll whenall, UniTask .WhenAny , UniTask .WhenEach它们就像Task.WhenAll / Task.WhenAny当时,返回类型更有用。它们返回值元组,因此您可以解构每个结果并传递多种类型。
public async UniTask Void LoadManyAsync ( )
{
// parallel load.
var ( a , b , c ) = await UniTask . WhenAll (
LoadAsSprite ( "foo" ) ,
LoadAsSprite ( "bar" ) ,
LoadAsSprite ( "baz" ) ) ;
}
async UniTask < Sprite > LoadAsSprite ( string path )
{
var resource = await Resources . LoadAsync < Sprite > ( path ) ;
return ( resource as Sprite ) ;
}
如果要将回调转换为UniTask ,则可以使用UniTask CompletionSource<T>这是TaskCompletionSource<T>轻量级版本。
public UniTask < int > WrapBy UniTask CompletionSource ( )
{
var utcs = new UniTask CompletionSource < int > ( ) ;
// when complete, call utcs.TrySetResult();
// when failed, call utcs.TrySetException();
// when cancel, call utcs.TrySetCanceled();
return utcs . Task ; //return UniTask <int>
}
您可以转换任务 - > UniTask : As UniTask , UniTask > UniTask <AsyncUnit> : AsAsyncUnit UniTask , UniTask <T> - > UniTask : As UniTask 。 UniTask <T> - > UniTask的转换成本是免费的。
如果要将异步转换为coroutine,则可以使用.ToCoroutine() ,如果您只允许使用Coroutine系统,这很有用。
UniTask不能等待两次。这与.NET标准2.1中引入的Valuetask/ivaluetasksource相似。
不应在Valuetask实例上执行以下操作:
- 多次等待实例。
- 多次致电ASTAKS。
- 使用.result或.getawaiter()。getResult()操作尚未完成或多次使用它们。
- 使用这些技术中的多种消耗实例。
如果您执行上述任何操作,则结果是不确定的。
var task = UniTask . DelayFrame ( 10 ) ;
await task ;
await task ; // NG, throws Exception
存储到类字段,您可以使用UniTask .Lazy支持多次调用。 .Preserve()允许多个呼叫(内部缓存结果)。当功能范围中有多个调用时,这很有用。
同样, UniTask CompletionSource可以等待多次,并等待许多呼叫者。
取消和例外处理
某些UniTask工厂方法具有CancellationToken cancellationToken = default参数。同样,某些统一的异步操作具有WithCancellation(CancellationToken)和To UniTask (..., CancellationToken cancellation = default)扩展方法的扩展方法。
您可以通过标准CancellationTokenSource将CancellationToken传递给参数。
var cts = new CancellationTokenSource ( ) ;
cancelButton . onClick . AddListener ( ( ) =>
{
cts . Cancel ( ) ;
} ) ;
await UnityWebRequest . Get ( "http://google.***co.jp" ) . SendWebRequest ( ) . WithCancellation ( cts . Token ) ;
await UniTask . DelayFrame ( 1000 , cancellationToken : cts . Token ) ;可以通过CancellationTokenSource或Monobehaviour的扩展方法GetCancellationTokenOnDestroy创建取消token。
// this CancellationToken lifecycle is same as GameObject.
await UniTask . DelayFrame ( 1000 , cancellationToken : this . GetCancellationTokenOnDestroy ( ) ) ;对于传播取消,所有异步方法都建议在最终参数中接受CancellationToken cancellationToken ,然后将CancellationToken从根到结束。
await FooAsync ( this . GetCancellationTokenOnDestroy ( ) ) ;
// ---
async UniTask FooAsync ( CancellationToken cancellationToken )
{
await BarAsync ( cancellationToken ) ;
}
async UniTask BarAsync ( CancellationToken cancellationToken )
{
await UniTask . Delay ( TimeSpan . FromSeconds ( 3 ) , cancellationToken ) ;
}
CancellationToken表示异步的生命周期。您可以持有自己的生命周期,而代替默认的comcellationTokenEndestroy。
public class MyBehaviour : MonoBehaviour
{
CancellationTokenSource disableCancellation = new CancellationTokenSource ( ) ;
CancellationTokenSource destroyCancellation = new CancellationTokenSource ( ) ;
private void OnEnable ( )
{
if ( disableCancellation != null )
{
disableCancellation . Dispose ( ) ;
}
disableCancellation = new CancellationTokenSource ( ) ;
}
private void OnDisable ( )
{
disableCancellation . Cancel ( ) ;
}
private void OnDestroy ( )
{
destroyCancellation . Cancel ( ) ;
destroyCancellation . Dispose ( ) ;
}
}在Unity 2022.2之后,Unity在monobehaviour.destroycancellationtoken和application.exitCancellationToken中添加了concellationToken。
当检测到取消时,所有方法都会抛出OperationCanceledException并在上游传播。如果不使用异步方法处理异常(不限于OperationCanceledException ),则最终将其传播到UniTask Scheduler.UnobservedTaskException 。未接收的例外的默认行为是将日志写为例外。可以使用UniTask Scheduler.UnobservedExceptionWriteLogType更改日志级别。unobservedExceptionWritElogType。如果要使用自定义行为,请将操作设置为UniTask Scheduler.UnobservedTaskException.
而且OperationCanceledException是一个特殊的例外,在UnobservedTaskException时,这被默默地忽略。
如果您想在异步UniTask方法中取消行为,请手动扔OperationCanceledException 。
public async UniTask < int > FooAsync ( )
{
await UniTask . Yield ( ) ;
throw new OperationCanceledException ( ) ;
}
如果您处理异常,但想忽略(传播到全局取消处理),请使用异常过滤器。
public async UniTask < int > BarAsync ( )
{
try
{
var x = await FooAsync ( ) ;
return x * 2 ;
}
catch ( Exception ex ) when ( ! ( ex is OperationCanceledException ) ) // when (ex is not OperationCanceledException) at C# 9.0
{
return - 1 ;
}
}
投掷/捕获OperationCanceledException略微沉重,因此,如果性能是一个问题,请使用UniTask .SuppressCancellationThrow ,以避免使用操作CanceLedException Throw。它返回(bool IsCanceled, T Result)而不是投掷。
var ( isCanceled , _ ) = await UniTask . DelayFrame ( 10 , cancellationToken : cts . Token ) . SuppressCancellationThrow ( ) ;
if ( isCanceled )
{
// ...
}
注意:仅当您直接呼叫到最源方法时,只会抑制投掷。否则,返回值将被转换,但是整个管道不会抑制投掷。
某些使用Unity播放器循环的功能,例如UniTask .Yield和UniTask .Delay等,确定了播放器循环上的comcellationToken状态。这意味着它不会在CancellationToken射击后立即取消。
如果您想更改此行为,则立即取消将其设置为参数,将cancelImmediately为comment。
await UniTask . Yield ( cancellationToken , cancelImmediately : true ) ;注意: cancelImmediately设置为True并检测立即取消的设置比默认行为更为昂贵。这是因为它使用CancellationToken.Register ;它比在播放器循环上检查取消token的重量更重。
超时处理
超时是取消的变体。您可以通过CancellationTokenSouce.CancelAfterSlim(TimeSpan)设置超时,然后将comcellationToken传递到异步方法。
var cts = new CancellationTokenSource ( ) ;
cts . CancelAfterSlim ( TimeSpan . FromSeconds ( 5 ) ) ; // 5sec timeout.
try
{
await UnityWebRequest . Get ( "http://**fo*o" ) . SendWebRequest ( ) . WithCancellation ( cts . Token ) ;
}
catch ( OperationCanceledException ex )
{
if ( ex . CancellationToken == cts . Token )
{
UnityEngine . Debug . Log ( "Timeout" ) ;
}
}
CancellationTokenSouce.CancelAfter是标准API。但是,在统一上,您不应该使用它,因为它取决于线程计时器。CancelAfterSlim是UniTask的扩展方法,它使用PlayerLoop。
如果要将超时与其他取消源一起使用,请使用CancellationTokenSource.CreateLinkedTokenSource 。
var cancelToken = new CancellationTokenSource ( ) ;
cancelButton . onClick . AddListener ( ( ) =>
{
cancelToken . Cancel ( ) ; // cancel from button click.
} ) ;
var timeoutToken = new CancellationTokenSource ( ) ;
timeoutToken . CancelAfterSlim ( TimeSpan . FromSeconds ( 5 ) ) ; // 5sec timeout.
try
{
// combine token
var linkedTokenSource = CancellationTokenSource . CreateLinkedTokenSource ( cancelToken . Token , timeoutToken . Token ) ;
await UnityWebRequest . Get ( "http://**fo*o" ) . SendWebRequest ( ) . WithCancellation ( linkedTokenSource . Token ) ;
}
catch ( OperationCanceledException ex )
{
if ( timeoutToken . IsCancellationRequested )
{
UnityEngine . Debug . Log ( "Timeout." ) ;
}
else if ( cancelToken . IsCancellationRequested )
{
UnityEngine . Debug . Log ( "Cancel clicked." ) ;
}
}优化以减少compellationTokenSource的分配,以进行按呼叫异步方法的超时,您可以使用UniTask的TimeoutController 。
TimeoutController timeoutController = new TimeoutController ( ) ; // setup to field for reuse.
async UniTask FooAsync ( )
{
try
{
// you can pass timeoutController.Timeout(TimeSpan) to cancellationToken.
await UnityWebRequest . Get ( "http://**fo*o" ) . SendWebRequest ( )
. WithCancellation ( timeoutController . Timeout ( TimeSpan . FromSeconds ( 5 ) ) ) ;
timeoutController . Reset ( ) ; // call Reset(Stop timeout timer and ready for reuse) when succeed.
}
catch ( OperationCanceledException ex )
{
if ( timeoutController . IsTimeout ( ) )
{
UnityEngine . Debug . Log ( "timeout" ) ;
}
}
}
如果要将超时与其他取消源一起使用,请使用new TimeoutController(CancellationToken) 。
TimeoutController timeoutController ;
CancellationTokenSource clickCancelSource ;
void Start ( )
{
this . clickCancelSource = new CancellationTokenSource ( ) ;
this . timeoutController = new TimeoutController ( clickCancelSource ) ;
}注意: UniTask具有.Timeout , .TimeoutWithoutException方法,但是,如果可能的话,请勿使用这些方法,请通过CancellationToken 。因为.Timeout .Timeout表示超时时忽略结果。如果将CancellationToken传递给该方法,它将从任务内部起作用,因此可以停止运行任务。
进步
统一的某些异步操作必须To UniTask (IProgress<float> progress = null, ...)扩展方法。
var progress = Progress . Create < float > ( x => Debug . Log ( x ) ) ;
var request = await UnityWebRequest . Get ( "http://google.***co.jp" )
. SendWebRequest ( )
. To UniTask ( progress : progress ) ;您不应使用标准的 通过命令行克隆项目:new System.Progress<T> 。使用Cysharp.Threading.Tasks.Progress 。该进度工厂有两种方法, Create和CreateOnlyValueChanged 。仅当进度值更改时, CreateOnlyValue