当前位置:   article > 正文

面试:浅析unity/xlua中的协程实现_unity xlua协程

unity xlua协程

问题来源:什么是协程程序https://blog.csdn.net/qq_33575542/article/details/82805857#%E4%BB%80%E4%B9%88%E6%98%AF%E5%8D%8F%E5%90%8C%E7%A8%8B%E5%BA%8F%EF%BC%9F

文章来源:https://mp.weixin.qq.com/s/K1xIxdJqjBD0ND8ShM2xMg

一、序言

在unity的游戏开发中,对于异步操作,有一个避免不了的操作: 协程,以前一直理解的懵懵懂懂,最近认真充电了一下,通过前辈的文章大体理解了一下,在这儿抛砖引玉写一些个人理解。好了,接下来就从一个小白的视角开始理解协程。

 

二、常见使用协程的示例

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

  1. public static void Download(System.Action finishCB){
  2.      string url = "https: xxxx";
  3.      StartCoroutine(DownloadFile(url));}private static IEnumerator DownloadFile(string url){
  4.     UnityWebRequest request = UnityWebRequest.Get(url);
  5.     request.timeout = 10;
  6.     yield return request.SendWebRequest();
  7.     if(request.error != null)      
  8.     {
  9.                Debug.LogErrorFormat("加载出错: {0}, url is: {1}", request.error, url);
  10.                request.Dispose();
  11.                yield break;
  12.      }
  13.    
  14.      if(request.isDone)
  15.      {
  16.            string path = "xxxxx";
  17.            File.WriteAllBytes(path, request.downloadHandler.data);
  18.            request.Dispose();
  19.            yiled break;
  20.      }}

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

1、关键词 IEnumerator

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

  1. public interface IEnumerator
  2. {
  3.     bool MoveNext();
  4.     void Reset();
  5.     Object Current{get;}
  6. }

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

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

2、关键词 Yield

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

  1. yield return <expresion>
  2. yiled break

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

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

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

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

 

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

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

  1. class Test
  2. {
  3.     static IEnumerator GetCounter()
  4.     {
  5.           for(int count = 0; count < 10; count++)
  6.           {
  7.                yiled return count;
  8.           }
  9.      }
  10. }

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

  1. internal class Test  {  
  2.    // GetCounter获得结果就是返回一个实例对象
  3.    private static IEnumerator GetCounter()  
  4.    {  
  5.        return new <GetCounter>d__0(0);  
  6.    }  
  7.  
  8.    // Nested type automatically created by the compiler to implement the iterator  
  9.    [CompilerGenerated]  
  10.    private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable  
  11.    {  
  12.        // Fields: there'll always be a "state" and "current", but the "count"  
  13.        // comes from the local variable in our iterator block.  
  14.        private int <>1__state;  
  15.        private object <>2__current;  
  16.        public int <count>5__1;  
  17.      
  18.        [DebuggerHidden]  
  19.        public <GetCounter>d__0(int <>1__state)  
  20.        {  
  21.           //初始状态设置
  22.            this.<>1__state = <>1__state;  
  23.        }  
  24.  
  25.        // Almost all of the real work happens here  
  26.        //类似于一个状态机,通过这个状态的切换,可以将整个迭代器执行过程中的堆栈等环境信息共享和保存
  27.        private bool MoveNext()  
  28.        {  
  29.            switch (this.<>1__state)  
  30.            {  
  31.                case 0:  
  32.                    this.<>1__state = -1;  
  33.                    this.<count>5__1 = 0;  
  34.                    while (this.<count>5__1 < 10)        //这里针对循环处理  
  35.                    {  
  36.                        this.<>2__current = this.<count>5__1;  
  37.                        this.<>1__state = 1;  
  38.                        return true;  
  39.                    Label_004B:  
  40.                        this.<>1__state = -1;  
  41.                        this.<count>5__1++;  
  42.                    }  
  43.                    break;  
  44.  
  45.                case 1:  
  46.                    goto Label_004B;  
  47.            }  
  48.            return false;  
  49.        }  
  50.  
  51.        [DebuggerHidden]  
  52.        void IEnumerator.Reset()  
  53.        {  
  54.            throw new NotSupportedException();  
  55.        }  
  56.  
  57.        void IDisposable.Dispose()  
  58.        {  
  59.        }  
  60.  
  61.        object IEnumerator<object>.Current  
  62.        {  
  63.            [DebuggerHidden]  
  64.            get  
  65.            {  
  66.                return this.<>2__current;  
  67.            }  
  68.        }  
  69.  
  70.        object IEnumerator.Current  
  71.        {  
  72.            [DebuggerHidden]  
  73.            get  
  74.            {  
  75.                return this.<>2__current;  
  76.            }  
  77.        }  
  78.    }  }

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

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

 

四、协程的衍生使用

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

  1. using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Profiling;public class QuotaCoroutine : MonoBehaviour{
  2.    // 每帧的额度时间,全局共享
  3.    static float frameQuotaSec = 0.001f;
  4.    static LinkedList<IEnumerator> s_tasks = new LinkedList<IEnumerator>();
  5.    // Use this for initialization
  6.    void Start()
  7.    {
  8.        StartQuotaCoroutine(Task(1, 100));
  9.    }
  10.    // Update is called once per frame
  11.    void Update()
  12.    {
  13.        ScheduleTask();
  14.    }
  15.    void StartQuotaCoroutine(IEnumerator task)
  16.    {
  17.        s_tasks.AddLast(task);
  18.    }
  19.    static void ScheduleTask()
  20.    {
  21.        float timeStart = Time.realtimeSinceStartup;
  22.        while (s_tasks.Count > 0)
  23.        {
  24.            var t = s_tasks.First.Value;
  25.            bool taskFinish = false;
  26.            while (Time.realtimeSinceStartup - timeStart < frameQuotaSec)
  27.            {
  28.                // 执行任务的一步, 后续没步骤就是任务完成
  29.                Profiler.BeginSample(string.Format("QuotaTaskStep, f:{0}", Time.frameCount));
  30.                taskFinish = !t.MoveNext();
  31.                Profiler.EndSample();
  32.                if (taskFinish)
  33.                {
  34.                    s_tasks.RemoveFirst();
  35.                    break;
  36.                }
  37.            }
  38.            // 任务没结束执行到这里就是没时间额度了
  39.            if (!taskFinish)
  40.                return;
  41.        }
  42.    }
  43.    IEnumerator Task(int taskId, int stepCount)
  44.    {
  45.        int i = 0;
  46.        while (i < stepCount)
  47.        {
  48.            Debug.LogFormat("{0}.{1}, frame:{2}", taskId, i, Time.frameCount);
  49.            i++;
  50.            yield return null;
  51.        }
  52.    }}

说一下思路: 在开始的时候,构建一个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(): 将协程挂起

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

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

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

看一下调用的API:

  1. local util = require 'xlua.util'
  2. local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
  3. CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
  4. local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))
  5. return {
  6.    start = function(...)
  7.    return cs_coroutine_runner:StartCoroutine(util.cs_generator(...))
  8. end;
  9. stop = function(coroutine)
  10.    cs_coroutine_runner:StopCoroutine(coroutine)
  11. end
  12. }

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

  1. local move_end = {}
  2. local generator_mt = {
  3.    __index = {
  4.        MoveNext = function(self)
  5.            self.Current = self.co()
  6.            if self.Current == move_end then
  7.                self.Current = nil
  8.                return false
  9.            else
  10.                return true
  11.            end
  12.        end;
  13.        Reset = function(self)
  14.            self.co = coroutine.wrap(self.w_func)
  15.        end
  16.    }
  17. }
  18. local function cs_generator(func, ...)
  19.    local params = {...}
  20.    local generator = setmetatable({
  21.        w_func = function()
  22.            func(unpack(params))
  23.            return move_end
  24.        end
  25.    }, generator_mt)
  26.    generator:Reset()
  27.    return generator
  28. end

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/95858
推荐阅读
相关标签
  

闽ICP备14008679号