同步处理:简单说就是代码按顺序执行,在方法1里调用方法2时,要等到方法2执行完毕才接着执行方法1的代码。
异步处理:简单说就是在两个方法里的代码同时或者来回执行,在方法1里调用方法2时,不等方法2执行完就接着执行接下来的代码。
异步处理不等于多线程,因为即使是单线程,也可以通过切换执行的代码来实现异步。典型的例子就是unity的协程。协程就是只运行在主线程来实现异步处理的。
而C#里真正跟多线程相关的是把ThreadPool封装后的Task类。Task类通常通过async/await 来实现异步,但异步和多线程是两个不同的概念。
这两个关键字是C#5.0引进的,本质是由编译器提供的语法糖,来方便进行异步编程用的。对于unity开发者来说,可以看成一个升级版的协程。
//协程版等待一秒
IEnumerator DelayCoroutine()
{
Debug.Log("Start");
yield return new WaitForSeconds(1f);
Debug.Log("End");
}
//Async版等待一秒
async void DelayTask()
{
Debug.Log("Start");
await Task.Delay(1000);
Debug.Log("End");
}
//异步方法,会在最后返回一个string
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);
}
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捕获异常,协程不行。
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可以返回值,如果中途取消,就可能导致后面的代码异常,所以只能抛异常。
虽然在Unity(2017版本以上)中可以正常地使用async/await和Task类,但是C#自带地Task类过于繁重而且一些unity里常用的功能要自己实现和封装。于是CySharp公司推出了UniTask来解决这个痛点。
用UniTask有以下优点:
但对Unity版本有要求,需要使用Unity2018.3以上版本。
对同一个UniTask实例不能两次await,不然会报错。
利用async/await 同C#的用法一样,只不过是将返回值改成相应的UniTask的结构体。
Task ——> UniTask
Task
void ——> UniTaskVoid //用于不需要返回UniTask的异步方法
利用UniTaskCompletionSource创建
用法如下:
async void Start()
{
var source = new UniTaskCompletionSource();
ReadyForCompleted(source).Forget();//只引发不考虑其是否完成
Debug.Log("Do Something...");
source.TrySetResult();//设置完成
//source.TrySetException(Exception);//设置失败
//source.TrySetCanceled();//设置取消
Debug.Log("Completed");
}
async UniTask ReadyForCompleted(UniTaskCompletionSource source)
{
Debug.Log("等待");
await source.Task;
Debug.Log("完成");
}
其实就是起一个Task,可以手动的设置是否完成,异常或者取消。
相应的有一个泛型类UniTaskCompletionSource
注意一旦执行了TrySet其中一个,则该实例再执行其他TrySet方法是无效果的。
注意:这个生成的UniTask是可以重复await的。
UniTask.Delay(1000); //延迟1000ms
UniTask.Delay(TimeSpan.FromSeconds(1));//延迟1s
UniTask.Delay(1000, delayTiming: PlayerLoopTiming.FixedUpdate);//以FixedUpdate的时间来等待
UniTask.DelayFrame(3);//等待3帧(默认 update循环)
UniTask.DelayFrame(3, PlayerLoopTiming.FixedUpdate);//等待3帧(Fixedupdate循环)
await UniTask.Yield();//等待update()下的一帧
Debug.Log(Time.time);
await UniTask.Yield(PlayerLoopTiming.FixedUpdate);//等待下一次fixedUpdate
//↓↓↓从这之后都是以fixupdate的时机来执行↓↓↓
Debug.Log(Time.time);
await UniTask.Yield();//等待一帧,回到update的时机来执行
//↓↓↓从这之后都是以update的时机来执行↓↓↓
Debug.Log(Time.time);
await UniTask.Yield();
//之后都在主线程跑
await UniTask.SwitchToThreadPool();
//之后都在线程池跑
await UniTask.SwitchToMainThread();
//之后回到主线程跑
yield和SwitchToMainThread区别在于,如果已经是主线程下的话,SwitchToMainThread不会再等待一帧,而yield无论是不是在主线程,都会等待1帧。
await UniTask.WaitUntil(()=> isActiveAndEnabled,PlayerLoopTiming.FixedUpdate);
var str = await UniTask.WaitUntilValueChanged(this.transform,x =>x.position);//第一个参数时判断目标,第二个参数是判断方法的委托。如果这个返回值变的话,即为发生变化。
Debug.Log(str);
注意:检测的target是一个弱引用,即可能会被GC回收。如果被GC回收的话,await就会被取消。
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);
///
/// 返回最先ping的IPAddress
///
///
///
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
用异步委托快速生成返回UniTask的异步方法。
UniTask.Create(
async ()=>
{
Debug.Log("aa");
await UniTask.Delay(1000);
return "11";
});
UniTask.Defer(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
return "11";
}
);
var asyncLazy = UniTask.Lazy(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
return "11";
}
);
await asyncLazy.Task;
UniTask.Void(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
}
);
UniTask.Action(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
}
);
等同于:
()=>
{
UniTask.Void(
async () =>
{
Debug.Log("aa");
await UniTask.Delay(1000);
}
);
};
//1秒内无法的话直接抛异常
var str = await DelayTask(token).Timeout(TimeSpan.FromSeconds(1));
//1秒内无法完成的话,await本身完成。
//同时complete = false
var (complete, result) = await DelayTask(token).TimeoutWithoutException(TimeSpan.FromSeconds(1));
对于一些需要用到等待的Unity对象提供GetAwaiter()功能,从而拿到Awaiter对象就可以进行await了。UniTask已经对各种各样的Unity对象进行了GetAwaiter的扩展。
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的wait
await SceneManager.LoadSceneAsync("NextScene");
//ResourceRequest的wait
await Resources.LoadAsync<Texture>("Icon").ToUniTask();
//AssetBundle加载的wait
await AssetBundle.LoadFromFileAsync("ABPath");
//UnityWebRequestAsyncOperation的wait
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);
public Button btn;
public Toggle tog;
public InputField inputField;
public Slider slider;
async void Start()
{
//获取token
var token = this.GetCancellationTokenOnDestroy();
//只想等待一次的话
await btn.OnClickAsync();
await tog.OnValueChangedAsync();
await inputField.OnEndEditAsync();
await slider.OnValueChangedAsync();
//想等待多次的话
//按键点击
var btnEventHandler = btn.GetAsyncClickEventHandler(token);
await btnEventHandler.OnClickAsync();
//Toggle状态更新
var togEventHandler = tog.GetAsyncValueChangedEventHandler(token);
await togEventHandler.OnValueChangedAsync();
//InputField输入完成
var inputEventHandler = inputField.GetAsyncEndEditEventHandler(token);
await inputEventHandler.OnEndEditAsync();
//slider更新
var sliderEventHandler = slider.GetAsyncValueChangedEventHandler(token);
await sliderEventHandler.OnValueChangedAsync();
}
//碰撞相关
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();
//Visible
var visibleTrigger = this.GetAsyncBecameVisibleTrigger();
var InvisibleTrigger = this.GetAsyncBecameInvisibleTrigger();
await visibleTrigger.OnBecameVisibleAsync();
await InvisibleTrigger.OnBecameInvisibleAsync();
await DoMove(...)
await(//同时执行两个Task,直到两个task都完成。
DoMove(...).ToUniTask();
DoMove(...).ToUniTask();
)
//生成Token
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
//将Token设成取消
tokenSource.Cancel();
//可以判断token是否取消了
if (token.IsCancellationRequested)
{
Debug.Log("Cancel");
}
token.ThrowIfCancellationRequested();//如果token是cancel的话,就抛出OperationCanceledException异常。
但每次都新生成一个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;
});
}
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就会变为失败
Window/UniTask Tracker,可以查看现在运行中的UniTask,确认是否有泄露的UniTask。