本文太乱,推荐frankjfwang的:全面解析Coroutine技术
Unity中的coroutine是通过yield expression;来实现的。官方脚本中到处会看到这种代码。
疑问:
yield是什么?
Coroutine是什么?
unity的coroutine程序运行流程怎么那么奇怪?
unity中的coroutine原理是什么,怎么实现的?
使用unity的coroutine须要注意什么问题?
一、yield的在几种语言中的程序运行特性:
Lua中的yield是使得协同函数执行->挂起而且传递參数给resume。resume使得协同函数挂起->执行而且传递參数给协同函数。
C#中yield return/break是用于函数查询集合生成器里面的值(类似迭代)返回,并记录当前现场,下次查询时从上一次记录的yield现场处,继续往下运行,直到继续往下运行没有了。那么退出这段yield的逻辑。
yield break会终止掉yield迭代逻辑并跳出。
YieldImplementation:
1).Caller callsfunction
2).Caller requestsitem 按需请求一个元素
3).Next itemreturned 返回请求的元素
4).Goto step #2
Python中的yield expression, 有yield的函数就变成了一个生成器,调用该函数返回的是迭代器对象,用迭代器对象调用next方法(或者循环中会自己主动调用next方法)。才開始运行函数,运行到yield expression处则中断。返回迭代器当前的值。并保留现场,下次调用next则从现场处開始运行,迭代完了就停止了。能够看出Python中的yield和C#中的yield是类似的,用于创建生成器,运行时中断返回迭代器值,并记录现场,下次从现场处继续运行。
Unity中的yield就是和C#,python中的类似。由于unity是基于.net框架的,且unity脚本開始是用Boo(Python的一个变种)写的。
仅仅是unity中多了coroutine特性类型。和StartCoroutine的coroutine管理类。StartCoroutine不是启动了一个新的线程。而是开启一个协同程序,默认unity全部代码都在一个线程中(http://answers.unity3d.com/questions/280597/new-thread-vs-startcoroutine.html)。
coroutine语言层面的原理:
在两年前,协程似乎是一个非常高级的东西,随后大多数语言或多或少都支持协程。我比較熟悉的有Python的gevent,Lua的coroutine,Go的goroutine。尤其是Lua和Go,语言本身就支持协程。协程也被叫做轻量级线程。
通俗点讲就是定义一大堆任务。然后通过一个线程轮着对每一个任务都执行一下,协作执行。它的厉害之处在于每执行到一个任务的时候。它都能够从这个任务上一次中断的地方開始执行。
在我们一般的印象中,仅仅有操作系统对线程进行调度的时候才会干这种事情,进行各种进栈,保存状态。
而协程,总共也仅仅是执行在一个线程中。要是使用线程本身的系统栈,早就暴了。因此在这里,实现的时候是用内存来模拟栈的操作。详细实现,我想复杂度一定会不小。
我们知道,线程比进程轻量级,因此产生一个线程消耗的资源比进程少,上下文切换也比进程节约。而协程比线程更加轻量级。上下文切换更是迅速。
于是在server编程方面给人无限想象。虽然眼下还没有出现一款主流的採用协程的webserver。可是Go语言开发的web服务的性能已经崭露头角了。
二、Unity的Coroutine运行现象:
第一种方法:
voidStart()
{
print("Starting " +Time.time);----------------------------------------1
StartCoroutine(WaitAndPrint(2));-------------------------------------2
print("Done " +Time.time);-------------------------------------------3
}
IEnumerator WaitAndPrint(float waitTime)
{
yield return new WaitForSeconds(waitTime);------------------------4
print("WaitAndPrint " + Time.time);----------------------------------5
}
该段代码的运行顺序是12435
运行到4协程注冊了事件,控制权交给外部线程。外部线程运行到3。事件发生,程序分段运行机制goto到协程处记录了堆栈信息运行5语句。
IEnumerator Start()
{
print("Starting " +Time.time);----------------------------------------1
yield return StartCoroutine(WaitAndPrint(2.0F));------------------------2
print("Done " +Time.time);------------------------------------------3
}
IEnumerator WaitAndPrint(float waitTime)
{
yield return new WaitForSeconds(waitTime);----------------------------4
print("WaitAndPrint " + Time.time);-----------------------------------------5
}
该段代码的运行顺序是12453
Why?这么奇怪的运行方式。
程序运行到4。运行yield return表达式注冊事件交出控制权给外部,由于外部还要交出控制权也须要运行yield return后面的表达式语句因此会重入WaitAndPrint函数接着协程当前状态下一步运行所以运行到5,yield return 后面表达式语句运行完成控制权全然交出,之后才运行3,根本原因是yield return 不能直接嵌套后面须要跟一个表达式(事件)。
三、Unity官方文档对coroutine的解释:
Normal coroutine updates are run after theUpdate function returns. Acoroutine is a function that can suspend its execution (yield) until the givenYieldInstruction finishes. Different
uses of Coroutines:
yield; The coroutine will continue after all Update functionshave been calledon the next frame.
yield WaitForSeconds(2); Continueafter a specified time delay, after all Update functions have been called for
theframe.
yield WaitForFixedUpdate(); Continue afterall FixedUpdate has been called on all scripts.
yield WWWContinue aftera WWW download has completed
yield StartCoroutine(MyFunc);
Chains the coroutine, and will wait for the MyFunc coroutine to completefirst.
C#要在yield coroutine之间加上returnkeyword。
四、Unity中的Coroutine原理推測:
虚拟机分段运行机制。 同类型嵌套用栈存放实现串行运行:.NET虚拟机在每一帧循环中, 会依次进入每一个编译器提前定义好的入口。对于Coroutine类型,编译器须要产生一些代码。在Coroutine类型指定的时间或事件完毕后(.net的虚拟机用函数指针进行标记管理现场和在流程中每帧检查时间或者事件满足后发送消息,将cpu全部权交给yield中断的现场。或是通过包括不同Coroutine迭代器的多个管理类管理各个coroutine,
每帧用coroutine子类通过多态检查时间或事件到达。将cpu全部权交给coroutine子类中断的现场)。从yield中断后的代码处继续往下运行, 这样就形成了我们看到的一个function能分段运行的机制。
而对于嵌套Coroutine类型,会串行的运行而不是并行的,可能.net虚拟机对于同coroutine类型用栈存放,栈顶的先运行,从而实现串行运行,假设外层的不使用yield return,那么不会串行运行,而是并行运行。
于是就能够解释上面样例中的运行次序问题。
原理图:
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQmx1ZXMxMDIx/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" >
见:http://www.richardfine.co.uk/2012/10/unity3d-monobehaviour-lifecycle/
事实上yield WaitForSeconds/null/WaitForEndOfFrame都是通过整个游戏While循环来驱动的,Yield仅仅是交出控制权给外部主线程且Coroutine设置事件挂起Coroutine协程(不是线程仅仅是分段运行机制)。当事件发生时候(不用下一帧到达。可是要在固定时间段内Update前或者后或Render后,超过了就会到下一帧)则进入协程里面的代码。
五、Unity中使用Coroutine须要注意的问题:
1.使用的地方和不能使用的地方:
必须在MonoBehaviour或继承于MonoBehaviour的类中调用 yield coroutine。
yield不能够在Update或者FixedUpdate里使用。
2.开启协程:
StartCoroutine(string methodName)和StartCoroutine(IEnumeratorroutine)都能够开启一个协程。
差别:
使用字符串作为參数时,开启协程时最多仅仅能传递一个參数,而且性能消耗会更大一点; 而使用IEnumerator 作为參数则没有这个限制。
3.删除协程:
1).在Unity3D中,使用StopCoroutine(stringmethodName)来终止该MonoBehaviour指定方法名的一个协同程序,使用StopAllCoroutines()来终止全部该MonoBehaviour能够终止的协同程序。
包含StartCoroutine(IEnumerator routine)的。
2).另一种方法能够终止协同程序,即将协同程序所在gameobject的active属性设置为false,当再次设置active为ture时,协同程序并不会再开启。
如是将协同程序所在脚本的enabled设置为false则不会生效。
4.js和C#中使用差别:
在C#中要使用 yield return而不是yield。
C#中yield(中断)语句必需要在IEnumerator类型里,C#方法的返回类型为IEnumerator。返回值如(eg:yield return new WaitForSeconds(2); 或者 yield returnnull);
5.协程函数返回值和參数类型。组合的设计模式:
协同程序的返回类型为Coroutine类型。在Unity3D中,Coroutine类继承于YieldInstruction,所以,协同程序的返回类型仅仅能为null、等待的帧数(frame)以及等待的时间。
协同程序的參数不能指定ref、out參数。可是,我们在使用WWW类时会常常使用到协同程序。因为在协同程序中不能传递參数地址(引用),也不能输出对象。
这使得每下载一个WWW对象都得重写一个协同程序。解决问题的方法是建立一个基于WWW的类(用组合模式来解决-事实上就是不通过函数传參全局关联一个对象了),并实现一个下载方法。例如以下:
using UnityEngine;
using System.Collections;
public class WWWObject : MonoBehaviour
{
public WWW www;
public WWWObject(string url)
{
if(GameVar.wwwCache)
www = WWW.LoadFromCacheOrDownload(url, GameVar.version);
else
www = new WWW(url);
}
public IEnumerator Load()
{
Debug.Log("Start loading : " + www.url);
while(!www.isDone)
{
if(GameVar.gameState == GameState.Jumping || GameVar.gameState ==GameState.JumpingAsync)
LoadScene.progress = www.progress;
yield return 1;
}
if(www.error != null)
Debug.LogError("Loading error : " + www.url + "\n" +www.error);
else
Debug.Log("End loading : " + www.url);
}
public IEnumerator LoadWithTip(string resourcesName)
{
Debug.Log("Start loading : " + www.url);
LoadScene.tipStr = "Downloading resources<" + resourcesName + "> . . .";
while(!www.isDone)
{
if(GameVar.gameState == GameState.Jumping || GameVar.gameState ==GameState.JumpingAsync)
LoadScene.progress= www.progress;
yield return 1;
}
if(www.error != null)
Debug.LogError("Loading error : " + www.url + "\n" +www.error);
else
Debug.Log("End loading : " + www.url);
}
}
调用:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class LoadResources : MonoBehaviour
{
static string url ="http://61.149.211.88/Package/test.unity3d";
public static WWW www = null;
IEnumerator Start()
{
if(!GameVar.resourcesLoaded)
{
GameVar.gameState = GameState.Jumping;
WWWObject obj = new WWWObject(url);
www = obj.www;
yield return StartCoroutine(obj.LoadWithTip("Textures"));
GameVar.resourcesLoaded = true;
GameVar.gameState = GameState.Run;
}
}
}
參考文章:
http://game.ceeger.com/forum/read.php?tid=13148
http://www.zhihu.com/question/23895384
http://blog.csdn.net/tkokof1/article/details/11842673
http://blog.csdn.net/tkokof1/article/details/12834939
http://www.richardfine.co.uk/2012/10/unity3d-monobehaviour-lifecycle/
http://dsqiu.iteye.com/blog/2029701