美文网首页
Unity中关于Coroutine与Async的使用问题

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

作者: 上善若水_2019 | 来源:发表于2020-01-18 15:26 被阅读0次

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

    这里要注意几点:

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

    return 0的情况有gc

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

    Log产生的gc

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

    所以,Unity原生协程可以放心使用,没有gc问题!!!真正有问题的是Debug.Log(),这东西在线上包内不要存在,严重影响性能!!!
    经网友提醒,以上结论有误,频繁调用startCoroutine会产生gc(我的例子是在一个协程里不断的yield,没有gc),原生的协程真的有点问题,建议自己写一套或者unity asset store下一个高效协程插件使用,不过呢,频繁调用(比如说每帧)startCoroutine真的合理么?

    在Update里每帧调用startCoroutine,gc是肯定有的

    至于为什么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的使用问题

          本文链接:https://www.haomeiwen.com/subject/rffhzctx.html