⾯试:浅析unityxlua中的协程实现
⼀、序⾔
在unity的游戏开发中,对于异步操作,有⼀个避免不了的操作: 协程,以前⼀直理解的懵懵懂懂,最近认真充电了⼀下,通过前辈的⽂章⼤体理解了⼀下,在这⼉抛砖引⽟写⼀些个⼈理解。好了,接下来就从⼀个⼩⽩的视⾓开始理解协程。
⼆、常见使⽤协程的⽰例
经常,我们会利⽤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();
!= null)
{
Debug.LogErrorFormat("加载出错: {0}, url is: {1}", , url);
request.Dispo();
yield break;
}
if(request.isDone)
{
string path = "xxxxx";
File.WriteAllBytes(path, request.downloadHandler.data);
request.Dispo();
yiled break;
老是头疼怎么回事女性
}}
这个例⼦中,⽤到了⼏个关键词: IEnumerator/yield return xxx/ yield break/StartCoroutine, 那么我们从这⼏个关键词⼊⼿,去理解这样的⼀个下载操作具体实现。
1、关键词 IEnumerator
这个关键词不是在Unity中特有,unity也是来⾃c#,所以找⼀个c#的例⼦来理解⽐较合适。⾸先看看IEnumerator的定义:
public interface IEnumerator
{
bool MoveNext();
void Ret();
Object Current{get;}
}
从定义可以理解,⼀个迭代器,三个基本的操作:Current/MoveNext/Ret, 这⼉简单说⼀下其操作的过程。在常见的集合中,我们使⽤foreach这样的枚举操作的时候,最开始,枚举数被定为在集合的第⼀个元素前⾯,Ret操作就是将枚举数返回到此位置。
迭代器在执⾏迭代的时候,⾸先会执⾏⼀个 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 aled 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)
{
ca 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;
ca 1:
goto Label_004B;
}
return fal;
}
[DebuggerHidden]
void IEnumerator.Ret()
房门图片{
throw new NotSupportedException();
}
void IDisposable.Dispo()
{
}
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, 如果为fal表明迭代执⾏完成了)。
通过这⼉,可以得到⼀个结论,协程并不是异步的,其本质还是在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>();
// U 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 = altimeSinceStartup;
while (s_tasks.Count > 0)
{
var t = s_tasks.First.Value;
bool taskFinish = fal;
while (altimeSinceStartup - 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.yield(): 将协程挂起
⽐较简易,可以写也给例⼦测试⼀下:
local func = function(a, b)
for i= 1, 5 do
print(i, a, b)
endendlocal func1 = function(a, b)
for i = 1, 5 do
print(i, a, b)
coroutine.yield()
鱼腥草怎么吃endendco = ate(sume(co, 1, 2)--此时会输出 1 ,1, 2/ 2,1,2/ 3, 1,2/4,1,2/5,1,2co1 = ate(func1)corouti
我们来看看xlua开源出来的util中对协程的使⽤⽰例⼜是怎么结合lua的协程,在lua端构建也给协程,让c#端也可以获取这个实例,从⽽添
加到unity端的主线程中去触发update。
看⼀下调⽤的API: