Unity中关于Coroutine与Async的使用问题

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(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 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(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");
            }
            
        }
    }

这里要注意几点:

  1. 方法用async标记后,如果方法内没有出现await,那么这个方法的调用和普通方法的调用没有区别。
  2. 有await时,在await之前的代码依然在主线程内按顺序执行,直到遇到await才线程阻塞。
  3. await可以理解为等待方法执行完成,除了标记async外,还能标记Task,表示等待该线程完成。所以await并不是针对async方法,而是针对async方法返回给我们的Task。
  4. async只能标记返回型为void、Task或者Task的方法。

由于我也是第一次用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发生

Unity中关于Coroutine与Async的使用问题_第1张图片
return 0的情况有gc

而我为了知道协程被开启了所以在协程里用了Debug.Log(),这个东西才是gc大户,直接产生了6.1kb的gc,我去!!!

Unity中关于Coroutine与Async的使用问题_第2张图片
Log产生的gc

而阅读Unity的日志可以知道,在Unity 5.3.6以前,协程的gc问题的确存在,但从Unity 5.3.6开始,这个问题已经被修复了!

所以,Unity原生协程可以放心使用,没有gc问题!!!真正有问题的是Debug.Log(),这东西在线上包内不要存在,严重影响性能!!!

至于为什么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 0return null是一样的,都是等一帧。其实,他们不一样,任何时候都推荐使用return null来等一帧。

完整示例在项目地址,打开Coroutine场景即可。

参考
Unity3D里foreach,using和Coroutine的GC问题探究及解决方案
Unity: Leveling up with Async / Await / Tasks
C#基础系列——异步编程初探:async和await

你可能感兴趣的:(Unity中关于Coroutine与Async的使用问题)