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.

相關文章
相關標籤/搜索