异步处理&同步处理
同步处理:简单说就是代码按顺序执行,在方法1里调用方法2时,要等到方法2执行完毕才接着执行方法1的代码。 异步处理:简单说就是在两个方法里的代码同时或者来回执行,在方法1里调用方法2时,不等方法2执行完就接着执行接下来的代码。
异步不等于多线程
异步处理不等于多线程,因为即使是单线程,也可以通过切换执行的代码来实现异步。典型的例子就是unity的协程。协程就是只运行在主线程来实现异步处理的。 而C#里真正跟多线程相关的是把ThreadPool封装后的Task类。Task类通常通过async/await 来实现异步,但异步和多线程是两个不同的概念。
async/await 和 Task
这两个关键字是C#5.0引进的,本质是由编译器提供的语法糖,来方便进行异步编程用的。对于unity开发者来说,可以看成一个升级版的协程。
IEnumerator DelayCoroutine()
{
Debug.Log("Start");
yield return new WaitForSeconds(1f);
Debug.Log("End");
}
async void DelayTask()
{
Debug.Log("Start");
await Task.Delay(1000);
Debug.Log("End");
}
async/await 与 Coroutine 相比的优点
- 由于是C#提供的功能,所以在非Mono脚本里也能实现异步。
- 可以方便的拿到异步的返回值。
async Task<string> DelayTask()
{
Debug.Log("Start");
await Task.Delay(1000);
Debug.Log("End");
return "Completed";
}
由于async可以在任何方法前加,同理适用于unity的生命周期函数。
async void Start()
{
var task = DelayTask();
Debug.Log("异步执行中..");
var str = await task;
Debug.Log(str);
}
- 避免回调地狱
有的时候我们希望在执行完异步操作时执行一个回调方法,但如果这个回调也有异步操作也要回调,就会造成回调的嵌套,降低代码的可读性。 协程的话可以将各个回调做成一个个小协程,之后在一个主协程里yield return。但是由于协程无法返回值,导致如果想要用上一个协程计算出的值的话,只能将回调作为委托传进去,无法避免回调的嵌套。 但async/await是可以返回值的,可以把回调改写成await的顺序执行。
async void Start()
{
var task = DelayTask();
Debug.Log($"异步执行中..");
var str = await task;
var task2 = AsyncFun2(str);
Debug.Log($"异步执行中..");
str = await task2();
Debug.Log(str);
}
4.async/await是可以用Try-Catch捕获异常,协程不行。
task 取消的问题
async/await需要明确地取消正在执行的异步方法,比较麻烦。 由于async/await异步实现是依靠着Task实例。Task实例是有可能是多线程的,由于线程是操作系统层面的资源就导致无法直接停止一个Task。所以我们只能做一个公共变量,task在执行异步时不断检查这个变量是否改变,改变的话说明要停止执行,在Task内部自己停止。 C#提供一个“取消标记”叫做CancellationTokenSource.Token,在创建task的时候传入此参数,就可以将主线程和任务相关联,然后在任务中设置“取消信号“叫做ThrowIfCancellationRequested来等待主线程使用Cancel来通知,一旦cancel被调用。task将会抛出OperationCanceledException来中断此任务的执行,最后将当前task的Status的IsCanceled属性设为true。 注意:一定要处理这个异常,可以通过调用Task.Result成员来获取这个异常。如果一直不查询Task的Exception属性。你的代码就永远注意不到这个异常的发生,如果不能捕捉到这个异常,垃圾回收时,抛出AggregateException,进程就会立即终止,这就是“牵一发动全身”,莫名其妙程序就自己关掉了,谁也不知道这是什么情况。所以,必须调用前面提到的某个成员,确保代码注意到异常,并从异常中恢复。因此可以将调用Task的某个成员来检查Task是否跑出了异常,通常调用Task的Result。 而协程只要把调用这个协程的GameObject删了就会停止协程。或者在开启协程时记下协程实例,要取消时调用StopCoroutine(coroutine)就行。主要原因就是await可以返回值,如果中途取消,就可能导致后面的代码异常,所以只能抛异常。
UniTask
虽然在Unity(2017版本以上)中可以正常地使用async/await和Task类,但是C#自带地Task类过于繁重而且一些unity里常用的功能要自己实现和封装。于是CySharp公司推出了UniTask来解决这个痛点。 用UniTask有以下优点:
- 用法和和原先的Task类用法一致。(Task-Like)
- 比Task更轻量,占用内存少。
- 对async/await 的优化,实现大幅减少GC。
- 提供unity相关的功能。
- 提供各种Awaiter。
- 实现在editor下await状态的可视化。(利用UniTaskTracker)
但对Unity版本有要求,需要使用Unity2018.3以上版本。 对同一个UniTask实例不能两次await,不然会报错。
生成UniTask实例的方法
-
利用async/await 同C#的用法一样,只不过是将返回值改成相应的UniTask的结构体。 Task ——> UniTask Task<T> ——> UniTask<T> void ——> UniTaskVoid //用于不需要返回UniTask的异步方法 -
利用UniTaskCompletionSource创建 用法如下:
async void Start()
{
var source = new UniTaskCompletionSource();
ReadyForCompleted(source).Forget();
Debug.Log("Do Something...");
source.TrySetResult();
Debug.Log("Completed");
}
async UniTask ReadyForCompleted(UniTaskCompletionSource source)
{
Debug.Log("等待");
await source.Task;
Debug.Log("完成");
}
其实就是起一个Task,可以手动的设置是否完成,异常或者取消。 相应的有一个泛型类UniTaskCompletionSource<T>,可以设置返回值。 注意一旦执行了TrySet其中一个,则该实例再执行其他TrySet方法是无效果的。 注意:这个生成的UniTask是可以重复await的。
- AutoResetUniTaskCompletionSource.Create()
2.0版本加入的一个UniTaskCompletionSource的池化版本,用法同UniTaskCompletionSource,只是获取实例的方法不同。而且这个只能await一次,因为要被回收走。适合在局部作用域里使用,随用随扔,
UniTask常用的静态方法
- UniTask.Run(Action)/ UniTask.Run(Function);
用法同对于的Task.Run方法,就是将委托内容方法放在线程池里运行。运行完毕后返回主线程(configawait设为true时)。 - UniTask.Delay
/返回一个延迟几秒完成的UniTask,能选择是以什么update时间来计算。
UniTask.Delay(1000);
UniTask.Delay(TimeSpan.FromSeconds(1));
UniTask.Delay(1000, delayTiming: PlayerLoopTiming.FixedUpdate);
- UniTask.DelayFrame //返回一个延迟几帧后完成的UniTask
UniTask.DelayFrame(3);
UniTask.DelayFrame(3, PlayerLoopTiming.FixedUpdate);
- UniTask.Yield() //等待1帧
可以用于将处理调回主线程用。例如yield之前是在其他线程跑,yield之后回到主线程跑。 默认是update循环。通过变更loop的类型,能切换之后代码的运行时机。
await UniTask.Yield();
Debug.Log(Time.time);
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);
Debug.Log(Time.time);
await UniTask.Yield();
Debug.Log(Time.time);
- UniTask.SwitchToThreadPool / UniTask.SwitchToMainThread
用来切换代码是在主线程跑还是线程池里跑。
await UniTask.Yield();
await UniTask.SwitchToThreadPool();
await UniTask.SwitchToMainThread();
yield和SwitchToMainThread区别在于,如果已经是主线程下的话,SwitchToMainThread不会再等待一帧,而yield无论是不是在主线程,都会等待1帧。
- UniTask.WaitUntil/UniTask.WaitWhile
类似与协程里用的WaitUntil和WaitWhile,可以指定是哪一个循环里Check。
await UniTask.WaitUntil(()=> isActiveAndEnabled,PlayerLoopTiming.FixedUpdate);
- UniTask.WaitUntilValueChanged
等到指定对象的参数发生变化时,才完成。
var str = await UniTask.WaitUntilValueChanged(this.transform,x =>x.position);
Debug.Log(str);
注意:检测的target是一个弱引用,即可能会被GC回收。如果被GC回收的话,await就会被取消。
- UniTask.WhenAll(List)
同Task.WhenAll()等待所有Task完成后完成,但UniTask版可以返回不同类型的值。
var num = UniTask.Run(()=>1);
var fl = UniTask.Run(()=>0.5f);
var str = UniTask.Run(()=>"aa");
var (p1, p2, p3) = await UniTask.WhenAll(num, fl, str);
- UniTask.WhenAny(List)
同Task.WhenAny()等待其中一个Task完成即为完成。
private async UniTask<IPAddress> SelectHostAsync(IPAddress[] apiHost)
{
var tasks = apiHost.Select(PingAsync).ToArray();
var (_, result) = await UniTask.WhenAny(tasks);
return result;
}
private async UniTask<IPAddress> PingAsync(IPAddress iP)
{
var ping = new Ping(iP.ToString());
while (!ping.isDone)
{
await UniTask.Yield();
}
return iP;
}
以下是2.0后加的方法 12. UniTask.Create<T>(Function(UniTask<T>)) 用异步委托快速生成返回UniTask的异步方法。
UniTask.Create(
async ()=>
{
Debug.Log("aa");
await UniTask.Delay(1000);
return "11";
});
- UniTask.Defer(Function(UniTask<T>))
用异步委托快速生成返回UniTask的异步方法,但在创建时不执行,但在await时才执行。
UniTask.Defer(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
return "11";
}
);
- UniTask.Lazy(Function(UniTask<T>))
用异步委托生成一个AsyncLazy型对象,在创建时不执行,但在await时才执行。与Defer不同的是这个可以重复await。
var asyncLazy = UniTask.Lazy(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
return "11";
}
);
await asyncLazy.Task;
- UniTask.Void(Function(UniTask<T>))
直接启动一个异步委托,不考虑其等待。
UniTask.Void(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
}
);
- UniTask.Action/UnityAction(Function(UniTask<T>))
就是将异步委托封装成Action或UnityAction。
UniTask.Action(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
}
);
等同于:
()=>
{
UniTask.Void(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
}
);
};
- uniTask.Timeout/TimeoutWithoutException()
UniTask的实例可以调用Timeout/TimeoutWithoutException()方法来控制超时。两个方法不同点在于抛不抛异常。
var str = await DelayTask(token).Timeout(TimeSpan.FromSeconds(1));
var (complete, result) = await DelayTask(token).TimeoutWithoutException(TimeSpan.FromSeconds(1));
Unity对象的扩展——Awaiter
对于一些需要用到等待的Unity对象提供GetAwaiter()功能,从而拿到Awaiter对象就可以进行await了。UniTask已经对各种各样的Unity对象进行了GetAwaiter的扩展。
- Coroutine的Awaiter
可以直接对协程方法进行await 来调用和等待。
async void Start()
{
await DelayCoroutine();
}
IEnumerator DelayCoroutine()
{
Debug.Log("Start");
yield return new WaitForSeconds(1f);
Debug.Log("End");
}
相应的,UniTask实例也可以转化成Coroutine。
IEnumerator DelayCoroutine()
{
Debug.Log("Start");
yield return UniTask.Delay(1000).ToCoroutine();
Debug.Log("End");
}
- AsyncOperation的Awaiter
Unity本身自带的一些异步方法,也可以用await了。 例如:
await SceneManager.LoadSceneAsync("NextScene");
await Resources.LoadAsync<Texture>("Icon").ToUniTask();
await AssetBundle.LoadFromFileAsync("ABPath");
var urw = UnityWebRequest.Get("http://unity.com/");
await urw.SendWebRequest();
如果需要检查加载的进度的话,要创建一个Progree实例传进去。
var progress = Progress.Create<float>(f => Debug.Log($"进度是:{f}"));
var urw = UnityWebRequest.Get("http://unity.com/");
await urw.SendWebRequest().ToUniTask(progress: progress);
- UGUI的一些响应方法也可以await
public Button btn;
public Toggle tog;
public InputField inputField;
public Slider slider;
async void Start()
{
var token = this.GetCancellationTokenOnDestroy();
await btn.OnClickAsync();
await tog.OnValueChangedAsync();
await inputField.OnEndEditAsync();
await slider.OnValueChangedAsync();
var btnEventHandler = btn.GetAsyncClickEventHandler(token);
await btnEventHandler.OnClickAsync();
var togEventHandler = tog.GetAsyncValueChangedEventHandler(token);
await togEventHandler.OnValueChangedAsync();
var inputEventHandler = inputField.GetAsyncEndEditEventHandler(token);
await inputEventHandler.OnEndEditAsync();
var sliderEventHandler = slider.GetAsyncValueChangedEventHandler(token);
await sliderEventHandler.OnValueChangedAsync();
}
- MonoBehaviour的回调函数也可以await
var collisionEnterTrigger = this.GetAsyncCollisionEnterTrigger();
var collisionExitTrigger = this.GetAsyncCollisionExitTrigger();
var collisionStayTrigger = this.GetAsyncCollisionStayTrigger();
var enter = await collisionEnterTrigger.OnCollisionEnterAsync();
var exit = await collisionExitTrigger.OnCollisionExitAsync();
var stay = await collisionStayTrigger.OnCollisionStayAsync();
var animatorIKTrigger = this.GetAsyncAnimatorIKTrigger();
var animatorMoveTrigger = this.GetAsyncAnimatorMoveTrigger();
var layerIndex = await animatorIKTrigger.OnAnimatorIKAsync();
await animatorMoveTrigger.OnAnimatorMoveAsync();
var visibleTrigger = this.GetAsyncBecameVisibleTrigger();
var InvisibleTrigger = this.GetAsyncBecameInvisibleTrigger();
await visibleTrigger.OnBecameVisibleAsync();
await InvisibleTrigger.OnBecameInvisibleAsync();
- DoTween也可以等待
从OpenUPM导入DOTween后,添加“UNITASK_DOTWEEN_SUPPORT”宏后可以用。
await DoMove(...)
await(
DoMove(...).ToUniTask();
DoMove(...).ToUniTask();
)
取消正在执行的异步的方法
- CancellationToken
这个实例本身就是C#用来控制Task取消的类。创建方法如下:
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
tokenSource.Cancel();
if (token.IsCancellationRequested)
{
Debug.Log("Cancel");
}
token.ThrowIfCancellationRequested();
但每次都新生成一个Token很麻烦,有时候就是想在脚本被销毁时,把挂在它身上的异步方法给停下来。
var token2 = this.GetCancellationTokenOnDestroy();
一旦UniTask被Cancel的话,UniTask就会在一个Cancel状态。且如果是在await的话,await之后的代码都不会执行。尽量不要省略这个token,在能传的异步方法里把这个传进去。 在一些方法里没有办法传token时就要手动在代码里去判断。例如:
private async UniTask<string> ReadTxtAsync(string path, CancellationToken token)
{
return await UniTask.Run(() =>
{
token.ThrowIfCancellationRequested();
var str = File.ReadAllText(path);
token.ThrowIfCancellationRequested();
return str;
});
}
- OperationCanceledException异常
在UniTask里抛出这个异常的话,UniTask就会处于Cancal状态。同时UniTask会吃掉这个异常,不会打出errorlog。 Cancel是一个外部操作,所以应该规定只有收到外部要求cancel时才能抛出这个异常。不应该程序内部自己判断来抛。同时如果在UniTask里try-catch时请把这个异常传出去,不要拦截。
private async UniTask TaskFunc(CancellationToken token)
{
try
{
await UniTask.Delay(1000, cancellationToken : token);
}
catch (Exception e) when(!(e is OperationCanceledException))
{
Debug.LogError("Error");
}
}
注意:这个异常只能用于Cancel时抛出,不应用于其他用途。 注意:在UniTask里抛出其他别的异常,UniTask就会变为失败
Editor下对UniTask的监控

Window/UniTask Tracker,可以查看现在运行中的UniTask,确认是否有泄露的UniTask。
|