unity协程coroutine浅析

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

1、序言c++

在unity的游戏开发中,对于异步操做,有一个避免不了的操做: 协程,之前一直理解的懵懵懂懂,最近认真充电了一下,经过前辈的文章大致理解了一下,在这儿抛砖引玉写一些我的理解。固然首先给出几篇写的很是精彩优秀的文章,最好认真拜读一下:web

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

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

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

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

 

2、常见使用协程的示例测试

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

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, 那么咱们从这几个关键词入手,去理解这样的一个下载操做具体实现。this

一、关键词 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编译成一个对象示例来执行,下面会讲解到。

二、关键词 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#编译器是如何把咱们写的协程调用编译生成的。

 

3、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。

4、协程的衍生使用

既然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中对于协程的实现。

 

5、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.

相关文章
相关标签/搜索