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.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM