zoukankan      html  css  js  c++  java
  • unity协程coroutine浅析

    转载请标明出处:http://www.cnblogs.com/zblade/

    一、序言

    在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下,通过前辈的文章大体理解了一下,在这儿抛砖引玉写一些个人理解。当然首先给出几篇写的非常精彩优秀的文章,最好认真拜读一下:

    王迅:Coroutine从入门到劝退​zhuanlan.zhihu.com  

    Unity3d中协程的原理,你要的yield return new xxx的真正理解之道​blog.csdn.net  

    Unity协程(Coroutine)原理深入剖析​dsqiu.iteye.com

    好了,接下来就从一个小白的视角开始理解协程。

     

    二、常见使用协程的示例

    经常,我们会利用monobehaviour的startcoroutine来开启一个协程,这是我们在使用unity中最常见的直观理解。在这个协程中执行一些异步操作,比如下载文件,加载文件等,在完成这些操作后,执行我们的回调。 举例说明:

    public static void Download(System.Action finishCB)
    {
          string url = "https: xxxx";
          StartCoroutine(DownloadFile(url));
    }
    
    private static IEnumerator DownloadFile(string url)
    {
         UnityWebRequest request = UnityWebRequest.Get(url);
         request.timeout = 10;
         yield return request.SendWebRequest();
         if(request.error != null)      
         {
                    Debug.LogErrorFormat("加载出错: {0}, url is: {1}", request.error, url);
                    request.Dispose();
                    yield break;
          }
         
          if(request.isDone)
          {
                string path = "xxxxx";
                File.WriteAllBytes(path, request.downloadHandler.data);
                request.Dispose();
                yiled break;
          }
    }

    这个例子中,用到了几个关键词: IEnumerator/yield return xxx/ yield break/StartCoroutine, 那么我们从这几个关键词入手,去理解这样的一个下载操作具体实现。

    1、关键词 IEnumerator

    这个关键词不是在Unity中特有,unity也是来自c#,所以找一个c#的例子来理解比较合适。首先看看IEnumerator的定义:

    public interface IEnumerator
    {
         bool MoveNext();
         void Reset();
         Object Current{get;}
    }

    从定义可以理解,一个迭代器,三个基本的操作:Current/MoveNext/Reset, 这儿简单说一下其操作的过程。在常见的集合中,我们使用foreach这样的枚举操作的时候,最开始,枚举数被定为在集合的第一个元素前面,Reset操作就是将枚举数返回到此位置。

    迭代器在执行迭代的时候,首先会执行一个 MoveNext, 如果返回true,说明下一个位置有对象,然后此时将Current设置为下一个对象,这时候的Current就指向了下一个对象。当然c#是如何将这个IEnumrator编译成一个对象示例来执行,下面会讲解到。

    2、关键词 Yield

    c#中的yield关键词,后面有两种基本的表达式:

    yield return <expresion>
    yiled break
    

    yield break就是跳出协程的操作,一般用在报错或者需要退出协程的地方。

    yield return是用的比较多的表达式,具体的expresion可以以下几个常见的示例:

    WWW : 常见的web操作,在每帧末调用,会检查isDone/isError,如果true,则 call MoveNext
    WaitForSeconds: 检测间隔时间是否到了,返回true, 则call MoveNext
    null: 直接 call MoveNext
    WaitForEndOfFrame: 在渲染之后调用, call MoveNext
    

    好了,有了对几个关键词的理解,接下来我们看看c#编译器是如何把我们写的协程调用编译生成的。

     

    三、c#对协程调用的编译结果

    这儿没有把上面的例子编译生成,就借用一下前面文章中的例子 :b

    class Test
    {
         static IEnumerator GetCounter()
         {
               for(int count = 0; count < 10; count++)
               {
                    yiled return count;
               }
          }
    }

    其编译器生成的c++结果:

    internal class Test  
    {  
        // GetCounter获得结果就是返回一个实例对象 
        private static IEnumerator GetCounter()  
        {  
            return new <GetCounter>d__0(0);  
        }  
      
        // Nested type automatically created by the compiler to implement the iterator  
        [CompilerGenerated]  
        private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable  
        {  
            // Fields: there'll always be a "state" and "current", but the "count"  
            // comes from the local variable in our iterator block.  
            private int <>1__state;  
            private object <>2__current;  
            public int <count>5__1;  
          
            [DebuggerHidden]  
            public <GetCounter>d__0(int <>1__state)  
            {  
               //初始状态设置
                this.<>1__state = <>1__state;  
            }  
      
            // Almost all of the real work happens here  
            //类似于一个状态机,通过这个状态的切换,可以将整个迭代器执行过程中的堆栈等环境信息共享和保存
            private bool MoveNext()  
            {  
                switch (this.<>1__state)  
                {  
                    case 0:  
                        this.<>1__state = -1;  
                        this.<count>5__1 = 0;  
                        while (this.<count>5__1 < 10)        //这里针对循环处理  
                        {  
                            this.<>2__current = this.<count>5__1;  
                            this.<>1__state = 1;  
                            return true;  
                        Label_004B:  
                            this.<>1__state = -1;  
                            this.<count>5__1++;  
                        }  
                        break;  
      
                    case 1:  
                        goto Label_004B;  
                }  
                return false;  
            }  
      
            [DebuggerHidden]  
            void IEnumerator.Reset()  
            {  
                throw new NotSupportedException();  
            }  
      
            void IDisposable.Dispose()  
            {  
            }  
      
            object IEnumerator<object>.Current  
            {  
                [DebuggerHidden]  
                get  
                {  
                    return this.<>2__current;  
                }  
            }  
      
            object IEnumerator.Current  
            {  
                [DebuggerHidden]  
                get  
                {  
                    return this.<>2__current;  
                }  
            }  
        }  
    }

    代码比较直观,相关的注释也写了一点,所以我们在执行开启一个协程的时候,其本质就是返回一个迭代器的实例,然后在主线程中,每次update的时候,都会更新这个实例,判断其是否执行MoveNext的操作,如果可以执行(比如文件下载完成),则执行一次MoveNext,将下一个对象赋值给Current(MoveNext需要返回为true, 如果为false表明迭代执行完成了)。

    通过这儿,可以得到一个结论,协程并不是异步的,其本质还是在Unity的主线程中执行,每次update的时候都会触发是否执行MoveNext。

    四、协程的衍生使用

    既然IEnumerator可以这样用,那我们其实可以只使用MoveNext和Current,就可以写一个简易的测试协程的例子,Ok,来写一个简易的例子,来自leader的代码,偷懒就复用了 :D

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.Profiling;
    
    public class QuotaCoroutine : MonoBehaviour
    {
        // 每帧的额度时间,全局共享
        static float frameQuotaSec = 0.001f;
    
        static LinkedList<IEnumerator> s_tasks = new LinkedList<IEnumerator>();
    
        // Use this for initialization
        void Start()
        {
            StartQuotaCoroutine(Task(1, 100));
        }
    
        // Update is called once per frame
        void Update()
        {
            ScheduleTask();
        }
    
        void StartQuotaCoroutine(IEnumerator task)
        {
            s_tasks.AddLast(task);
        }
    
        static void ScheduleTask()
        {
            float timeStart = Time.realtimeSinceStartup;
            while (s_tasks.Count > 0)
            {
                var t = s_tasks.First.Value;
                bool taskFinish = false;
                while (Time.realtimeSinceStartup - timeStart < frameQuotaSec)
                {
                    // 执行任务的一步, 后续没步骤就是任务完成
                    Profiler.BeginSample(string.Format("QuotaTaskStep, f:{0}", Time.frameCount));
                    taskFinish = !t.MoveNext();
                    Profiler.EndSample();
    
                    if (taskFinish)
                    {
                        s_tasks.RemoveFirst();
                        break;
                    }
                }
    
                // 任务没结束执行到这里就是没时间额度了
                if (!taskFinish)
                    return;
            }
        }
    
        IEnumerator Task(int taskId, int stepCount)
        {
            int i = 0;
            while (i < stepCount)
            {
                Debug.LogFormat("{0}.{1}, frame:{2}", taskId, i, Time.frameCount);
                i++;
                yield return null;
            }
        }
    }

    说一下思路: 在开始的时候,构建一个IEnuerator实例塞入链表中,然后再后续的每帧update的时候,取出这个实例,执行一次MoveNext,一直到都执行完后,移除这个实例,这样就不用显示的调用StartCoroutine,也可以类似的触发执行MoveNext :D

    看运行结果:

    可行。OK,关于unity的协程就写到这儿了,接下来将一下xlua中对于协程的实现。

     

    五、Lua中的协程

    Lua中的协程和unity协程的区别,最大的就是其不是抢占式的执行,也就是说不会被主动执行类似MoveNext这样的操作,而是需要我们去主动激发执行,就像上一个例子一样,自己去tick这样的操作。

    Lua中协程关键的三个API:

    coroutine.create()/wrap: 构建一个协程, wrap构建结果为函数,create为thread类型对象

    coroutine.resume(): 执行一次类似MoveNext的操作

    coroutine.yield(): 将协程挂起

    比较简易,可以写也给例子测试一下:

    local func = function(a, b)
        for i= 1, 5 do
            print(i, a, b)
        end
    end
    
    local func1 = function(a, b)
        for i = 1, 5 do
            print(i, a, b)
            coroutine.yield()
        end
    end
    
    
    co =  coroutine.create(func)
    coroutine.resume(co, 1, 2)
    --此时会输出 1 ,1, 2/ 2,1,2/ 3, 1,2/4,1,2/5,1,2
    
    co1 = coroutine.create(func1)
    coroutine.resume(co1, 1, 2)
    --此时会输出 1, 1,2 然后挂起
    coroutine.resume(co1, 3, 4)
    --此时将上次挂起的协程恢复执行一次,输出: 2, 1, 2 所以新传入的参数3,4是无效的

    我们来看看xlua开源出来的util中对协程的使用示例又是怎么结合lua的协程,在lua端构建也给协程,让c#端也可以获取这个实例,从而添加到unity端的主线程中去触发update。

    看一下调用的API:

    local util = require 'xlua.util'
    
    local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
    CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
    local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))
    
    return {
        start = function(...)
            return cs_coroutine_runner:StartCoroutine(util.cs_generator(...))
        end;
    
        stop = function(coroutine)
            cs_coroutine_runner:StopCoroutine(coroutine)
        end
    }

    start操作,本质就是将function包一层,调用util.csgenerator,进一步看看util中对cs_generator的实现

    local move_end = {}
    
    local generator_mt = {
        __index = {
            MoveNext = function(self)
                self.Current = self.co()
                if self.Current == move_end then
                    self.Current = nil
                    return false
                else
                    return true
                end
            end;
            Reset = function(self)
                self.co = coroutine.wrap(self.w_func)
            end
        }
    }
    
    local function cs_generator(func, ...)
        local params = {...}
        local generator = setmetatable({
            w_func = function()
                func(unpack(params))
                return move_end
            end
        }, generator_mt)
        generator:Reset()
        return generator
    end

    代码很短,不过思路很清晰,首先构建一个table, 其中的key对应一个function,然后修改去元表的_index方法,其中包含了MoveNext函数的实现,也包含了Reset函数的实现,不过这儿的Reset和IEnumerator的不一样,这儿是调用coroutine.wrap来生成一个协程。这样c#端获取到这个generator的handleID后,后面每帧update回来都会执行一次MoveNext,如果都执行完了,这时候会return move_end,表明协程都执行完了,返回false给c#端清空该协程的handleID.

  • 相关阅读:
    [设计模式]<<设计模式之禅>>关于迪米特法则
    [设计模式]<<设计模式之禅>>关于接口隔离原则
    [设计模式]<<设计模式之禅>>关于依赖倒置原则
    /proc/meminfo分析(一)
    Dynamic DMA mapping Guide
    Linux时钟
    Linux系统休眠和设备中断处理
    Linux调度器
    Linux调度器
    Linux标识进程
  • 原文地址:https://www.cnblogs.com/zblade/p/9857808.html
Copyright © 2011-2022 走看看