轉載請標明出處:http://www.cnblogs.com/zblade/node
1、序言c++
在unity的遊戲開發中,對於異步操做,有一個避免不了的操做: 協程,之前一直理解的懵懵懂懂,最近認真充電了一下,經過前輩的文章大致理解了一下,在這兒拋磚引玉寫一些我的理解。固然首先給出幾篇寫的很是精彩優秀的文章,最好認真拜讀一下:web
Unity3d中協程的原理,你要的yield return new xxx的真正理解之道 app
好了,接下來就從一個小白的視角開始理解協程。函數
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.