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<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问题!?。≌嬲形侍獾氖?code>Debug.Log(),这东西在线上包内不要存在,严重影响性能!??!
经网友提醒,以上结论有误,频繁调用startCoroutine会产生gc(我的例子是在一个协程里不断的yield,没有gc),原生的协程真的有点问题,建议自己写一套或者unity asset store下一个高效协程插件使用,不过呢,频繁调用(比如说每?。?code>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

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容