Coroutine(协程)我想大家都很熟悉了,由于Unity是单线程的引擎,我们在做一些异步操作的时候都是靠着协程来办到的。然而,随着Unity更新到2017版本及以上的版本,Runtime可以支持到.NET 4.x Equivalent时,C#中的异步操作就可以使用Thread的升级版Task以及async、await这些东西了。
我们先来看一个很普通场景,从网上获取一些json数据到本地(这个例子里我都从https://jsonplaceholder.typicode.com/users获取数据),以便做进一步处理,通常我们会开一个协程,比如这样
IEnumerator FetchData(){
Users[] users;
// USERS
UnityWebRequest www = UnityWebRequest.Get(USERS_URL);
yield return www.SendWebRequest();
if (www.isHttpError || www.isNetworkError)
{
Debug.Log("A network error occurred");
yield break;
}
string json = www.downloadHandler.text;
try
{
users = JsonHelper.GetJsonArray<Users>(json);
}
catch
{
Debug.Log("An error occurred");
yield break;
}
// OUTPUT
foreach (Users user in users)
{
Debug.Log(user.name);
}
}
然后在需要的时候调用
void Update()
{
if(Input.GetKeyDown(KeyCode.Space)){
StartCoroutine(FetchData());
}
}
这样写代码大概算是天经地义了。然而,Coroutine也有它自己的不足。首先,它无法返回值,或者说需要通过一些比较复杂的方法才能使它能够返回值,这样我们不得不写一个很长的单体式协程(monolithic coroutine,大概是这么翻译的吧,我也不清楚);其次,yield无法放入try catch中,我们不得不创建一个混合了同步(try catch)与异步(www.isHttpError或www.isNetworkError)的错误处理机制。
所以,在当下的Unity版本中,有时候我们能用异步编程来代替协程,以此来规避一些协程所带来的困扰。想要使用异步,首先要确保Runtime版本,在Unity2017及以上的版本中,点击Edit > Project Settings > Player > Configuration > Scripting Runtime Version > .NET 4.x Equivalent。如果你在使用2017以前的Unity版本,很遗憾,这个新功能你无法使用了o(╥﹏╥)o
那么现在我们来改写之前的协程,将它变成异步
async Task<Users[]> FetchUsers(){
UnityWebRequest www = UnityWebRequest.Get(USERS_URL);
www.SendWebRequest();
while (!www.isDone){
await Task.Delay(100);
}
if (www.isHttpError || www.isNetworkError)
{
throw new System.Exception();
}
string json = www.downloadHandler.text;
Users[] res = JsonHelper.GetJsonArray<Users>(json);
return res;
}
然后是调用
async void LateUpdate() {//这里的方法不标记为async则下面会报错
if(Input.GetKeyDown(KeyCode.L)){
try
{
//方法不标记为async则报错:The 'await' operator can only be used within
//an async method. Consider marking this method with the 'async' modifier
//and changing its return type to 'Task'.
Users[] users = await FetchUsers();
for (int i = 0; i < users.Length; i++)
{
Debug.Log(users[i].name);
}
}
catch (System.Exception)
{
Debug.Log("An error occurred");
}
}
}
这里要注意几点:
- 方法用async标记后,如果方法内没有出现await,那么这个方法的调用和普通方法的调用没有区别。
- 有await时,在await之前的代码依然在主线程内按顺序执行,直到遇到await才线程阻塞。
- await可以理解为等待方法执行完成,除了标记async外,还能标记Task,表示等待该线程完成。所以await并不是针对async方法,而是针对async方法返回给我们的Task。
- async只能标记返回型为void、Task或者Task<T>的方法。
由于我也是第一次用C#的异步,所有的一切对我来说也很新,如果有希望了解更多的小伙伴们,可以去官方看相关文档
Asynchronous programming with async and await
async (C# Reference)
await operator (C# reference)
最后,我本来以为这个东西可以替代Coroutine的另一个原因是Coroutine有gc,网上一查有关协程gc的博客一大堆。然而,我亲自试验了一遍,写了个最简单的协程来测试gc情况,发现协程的gc问题是。。。根本没有问题?。?!
private static WaitForSecondsRealtime w = new WaitForSecondsRealtime(1);
IEnumerator Counter(){
for (int i = 0; i < 100000; i++)
{
// Debug.Log(i);
yield return null;
// yield return 0;
// yield return w;
// yield return new WaitForSeconds(1);
}
}
以上是我试验的好几种情况,返回null
、返回一个数字、返回一个new WaitForSeconds(1)
,将new WaitForSecondsRealtime(1)
作为全局变量使用,都试了一遍后,发现只有在return 0
的情况下才会发生gc,其余情况都不会有gc发生。
而我为了知道协程被开启了所以在协程里用了Debug.Log()
,这个东西才是gc大户,直接产生了6.1kb的gc,我去?。?!
而阅读Unity的日志可以知道,在Unity 5.3.6以前,协程的gc问题的确存在,但从Unity 5.3.6开始,这个问题已经被修复了!
所以,Unity原生协程可以放心使用,没有gc问题!?。≌嬲形侍獾氖?code>Debug.Log(),这东西在线上包内不要存在,严重影响性能!??!
经网友提醒,以上结论有误,频繁调用startCoroutine
会产生gc(我的例子是在一个协程里不断的yield,没有gc),原生的协程真的有点问题,建议自己写一套或者unity asset store下一个高效协程插件使用,不过呢,频繁调用(比如说每?。?code>startCoroutine真的合理么?
至于为什么return 0
会有gc,那是因为return 0
发生了装箱拆箱操作,不可避免的产生了gc,StackOverFlow上有人回答了这个问题,地址在https://stackoverflow.com/questions/39268753/what-is-the-difference-between-yield-return-0-and-yield-return-null-in-corou,所以不要认为return 0
和return null
是一样的,都是等一帧。其实,他们不一样,任何时候都推荐使用return null
来等一帧。
完整示例在项目地址,打开Coroutine场景即可。
参考
Unity3D里foreach,using和Coroutine的GC问题探究及解决方案
Unity: Leveling up with Async / Await / Tasks
C#基础系列——异步编程初探:async和await