目录
协程类
自定义的IEnumerator
拓展-StClusterThread
如果你不了解迭代器与yield return的相关知识,可以先从之前的一篇文章《从迭代器到Unity的Coroutines》看起。
实际上Unity提供的协程足够的好用,通常来说我们没必要去重复写一个自己的协程。
但是通常问题都有但是,Unity并没有提供接口对单独某个协程进行操作,我们无法从外部挂起一个协程,因此这里简单实现一个协程,方便我们更好的管理正在运行的协程。
简单实现了一个StThread类,作为我们的协程实例(Coroutine这个单词太长了,所以我偷懒用了Thread :) )。
public class StThread
{
protected enum EState
{
None,
Running,
Finished,
};
protected EState mState;
protected Stack mContextStack;
public StThread()
{
mContextStack = new Stack();
}
public IEnumerator RunThread()
{
ThreadBegin();
while(true)
{
if(InternalRun())
{
break;
}
yield return null;
}
ThreadEnd();
}
public bool IsFinished()
{
return mState == EState.Finished;
}
protected virtual IEnumerator Run()
{
yield return null;
}
protected virtual bool InternalRun()
{
bool isDone = true;
while(mContextStack.Count > 0)
{
IEnumerator top = mContextStack.Peek();
while(isDone = !top.MoveNext())
{
//If one stack frame done, we will try to make it run
mContextStack.Pop();
if(mContextStack.Count > 0)
{
top = mContextStack.Peek();
}
else
{
break;
}
}
//IEnumerator.Current always hold the last yield return value.
if(top.Current != null)
{
mContextStack.Push((IEnumerator)top.Current);
Debug.Assert(mContextStack.Peek().Current == null, "New Stack Frame run before push To Context Stack.");
//Force run again to avoid yield return CallFunction one frame delay problem.
continue;
}
break;
}
return isDone;
}
protected virtual void ThreadBegin()
{
Debug.Assert(mState == EState.None, "Try to run uninitialized thread.");
mContextStack.Clear();
mContextStack.Push(Run());
mState = EState.Running;
}
protected virtual void ThreadEnd()
{
mState = EState.Finished;
Debug.Assert(mContextStack.Count == 0, "Not all stack frame finished.");
}
}
代码不长,主要逻辑在RunThread中。
首先简要回顾一下yield return。yield return会让函数返回一个迭代器实例,它保存了函数运行的上下文信息,我们可以通过MoveNext来继续执行函数。
我们用一个栈mContextStack来保存运行时产生的迭代器(为什么需要保存多个迭代器我们后面会详细解释)。
另外由于是测试,我直接使用了成员函数Run作为我们需要运行的协程函数,实际上使用时,我们可以传入一个外部的委托作为协程函数。
让我们结合例子来了解它是怎么工作的,为了方便测试,我写了两个函数用于完成等待的功能。
protected IEnumerator Wait(float time)
{
Stopwatch sw = new Stopwatch();
sw.Start();
long waitMilliseconds = (long)time * 1000;
while(sw.ElapsedMilliseconds < waitMilliseconds)
{
yield return null;
}
Console.WriteLine("Wait Done");
}
protected IEnumerator WaitFrame()
{
yield return null;
Console.WriteLine("WaitFrame");
}
我把它们直接作为StThread的成员函数,只需要调用yield return Wait(3.0)即可等待3秒(之后我介绍如何封装成实例的形式,想Unity一样用yield return new WaitForSeconds(3.0)的形式来进行等待)。
public class ThreadB : StThread
{
protected override IEnumerator Run()
{
yield return SubFunction0();
Console.WriteLine("[{0}]ThreadB wait for 1 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return Wait(1.0f);
Console.WriteLine("[{0}]ThreadB wait for 2 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return Wait(2.0f);
Console.WriteLine("[{0}]ThreadB wait done", msStopWatch.Elapsed.TotalSeconds);
}
IEnumerator SubFunction0()
{
yield return SubFunction1();
Console.WriteLine("[{0}]ThreadB SubFunction0 wait for 1 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return Wait(1.0f);
Console.WriteLine("[{0}]ThreadB SubFunction0 wait for 2 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return Wait(2.0f);
Console.WriteLine("[{0}]ThreadB SubFunction0 wait done", msStopWatch.Elapsed.TotalSeconds);
}
IEnumerator SubFunction1()
{
Console.WriteLine("[{0}]ThreadB SubFunction1 wait for 1 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return null;
}
};
定义了一个子类ThreadB,然后用如下的方法运行
var enumerator = new ThreadB().RunThread();
while(enumerator.MoveNext())
{
}
运行输出如下
第一帧执行到SubFunction1,第二帧SubFunction1执行完毕,返回SubFunction0执行到Wait(1.0f),等待1秒之后继续等待2秒,SubFunction0执行完毕返回Run执行Run函数,和我们预期的行为相同。
回过头看StThread,
var enumerator = new ThreadB().RunThread();
第一步执行RunThread时,实际上并没有执行这个函数,只是返回一个迭代器作为函数执行的入口,实际上第一次执行是在MoveNext时发生的。
第一次MoveNext时,开始执行RunThread,
首先调用初始化ThreadBegin()协程
protected virtual void ThreadBegin()
{
Debug.Assert(mState == EState.None, "Try to run uninitialized thread.");
mContextStack.Clear();
mContextStack.Push(Run());
mState = EState.Running;
}
这里同样没有运行Run函数,只是把函数的入口存入上下文栈中,然后RunThread函数进入到一个while循环中
while(true)
{
if(InternalRun())
{
break;
}
yield return null;
}
当InternalRun返回true时代表线程执行完成,终止循环,线程结束,否则调用yield return保存当前上下文从而下一次能继续执行循环。
重点看InternalRun函数,
protected virtual bool InternalRun()
{
bool isDone = true;
while(mContextStack.Count > 0)
{
IEnumerator top = mContextStack.Peek();
while(isDone = (!top.MoveNext()))
{
//If one stack frame done, we will try to make it run
mContextStack.Pop();
if(mContextStack.Count > 0)
{
top = mContextStack.Peek();
}
else
{
break;
}
}
//IEnumerator.Current always hold the last yield return value.
if(top.Current != null)
{
mContextStack.Push((IEnumerator)top.Current);
Debug.Assert(mContextStack.Peek().Current == null, "New Stack Frame run before push To Context Stack.");
//Force run again to avoid yield return CallFunction one frame delay problem.
continue;
}
break;
}
return isDone;
}
第一步从上下文栈中取栈顶,运行栈顶函数
isDone = (!top.MoveNext())
当MoveNext返回false时代表迭代结束,函数调用完成。
首先我们必须明确何时MoveNext返回false,看下面这个函数
IEnumerator SubFunction1()
{
Console.WriteLine("[{0}]ThreadB SubFunction1 wait for 1 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return null;
//end function
}
虽然yield return null之后没有执行任何功能,但实际上这个函数需要迭代两次才能执行完成,即执行到'}'处时MoveNext返回false。
其次我们要明确迭代器的Current中存放的到底是什么值。Current中存放的是最后一次yield return返回的值。
明确这两点后,我们来看ThreadB第一帧时发生了什么。
按照我们的预期,第一帧应该执行到,
Run->
SubFunction0->
SubFunction1->
Console.WriteLine("[{0}]ThreadB SubFunction1 wait for 1 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return null;
Run的MoveNext第一次执行,到第一个yield return
Run()->
yield return SubFunction0();
此时top.Current是SubFunction0的入口,所以我们把入口存放到上下文栈的栈顶中,即我们获得了一个新的栈帧。由于此时SubFunction0并没有运行过,如果此时就退出当前的帧,那么实际上需要3帧才能执行到SubFunction1的yield return,这是我们所不希望的,所以一旦有新的栈帧产生时,我们要让新的函数先运行一次,因此这里使用continue让程序回到循环中,执行一次SubFunction0。
同理SubFunction0会在yield return SubFunction1();时返回,此时top.Current中存放的是SubFunction1的入口,所以还需要执行一次循环,运行一次SubFunction1,所以第一帧时,这个while循环一共会执行三次,从而避免使用yield return调用函数时的延迟问题。
但是实际上这里是有问题的,因为我们默认yield return返回的值,一定是IEnumerator。实际上我们应该阻止使用者返回一些奇怪的值,所以在判断top.Current应该注意内容的正确性。
在大循环内部,同样还有一个小循环,这个小循环是为了消除函数执行完毕时帧延迟的问题,
看SubFunction1结束的帧,
预期的行为是
SubFunction1->
{
SubFunction0->
Console.WriteLine("[{0}]ThreadB SubFunction0 wait for 1 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return Wait(1.0f);
在SubFunction1将要执行完毕时,上下文栈中共存放三个栈帧,从底到顶依次是
----------------SubFunction1()----------------
yield return null;
->执行位置
}
----------------SubFunction0()----------------
yield return SubFunction1();
->执行位置
Console.WriteLine("[{0}]ThreadB SubFunction0 wait for 1 seconds", msStopWatch.Elapsed.TotalSeconds);
yield return Wait(1.0f);
----------------Run()----------------
yield return WaitFrame();
->执行位置
Console.WriteLine("ThreadA wait for 3 seconds");
SubFunction1执行完毕,返回false,由于上下文栈顶端的栈帧已经执行完毕,所以我们将它出栈,那么此时位于栈顶的是SubFunction0,我们继续执行top的函数,所以SubFunction0会执行到下一个yield return。
由于SubFunction0此时未结束,所以这个while循环不会继续执行下去,正常进入到下一帧。
但是这里有仍有一个小问题,假设我们有这样一个协程,
public class ThreadA:StThread
{
protected override IEnumerator Run()
{
yield return WaitFrame();
Console.WriteLine("ThreadA wait for 3 seconds");
yield return Wait(3.0f);
Console.WriteLine("ThreadA wait done");
}
};
我们之前说过,迭代器的Current中,存放的是最后一个yield return的返回值,所以当ThreadA的Run函数执行完毕是,在判断时会发现top.Current != null,于是把wait(3.0)重新入栈,又从外部的While循环又执行了一遍。不过所幸,wait(3.0)函数已经执行完毕,再次调用MoveNext并不会发生任何事情,所以这里不会引起任何的bug。
但是我们可以在判断top.Current之前,先判断一次mContextStack.Count,只有当栈中还有内容时才会去迭代top.Current中的值。即可避免而外一次的While循环调用。
因为过于简单粗暴,所以直接贴代码。
public class StWait:IEnumerator
{
public object Current
{
get
{
return mEnumerator.Current;
}
}
IEnumerator mEnumerator;
public StWait(float time)
{
mEnumerator = Wait(time);
}
public bool MoveNext()
{
return mEnumerator.MoveNext();
}
public void Reset()
{
mEnumerator.Reset();
}
IEnumerator Wait(float time)
{
Stopwatch sw = new Stopwatch();
sw.Start();
long waitMilliseconds = (long)time * 1000;
while(sw.ElapsedMilliseconds < waitMilliseconds)
{
yield return null;
}
}
}
另外我还写了一个支持子协程的类,有兴趣的可以直接看代码。
public class StClusterThread : StThread
{
List mChildThread;
List mChildContext;
bool mWaitChild;
public StClusterThread()
{
mChildThread = new List();
mChildContext = new List();
mWaitChild = true;
}
public void AddChildThread(StThread child)
{
mChildThread.Add(child);
mChildContext.Add(child.RunThread());
}
protected override bool InternalRun()
{
int currentChildNum = mChildThread.Count;
for(int i = 0; i < currentChildNum; ++i)
{
if(mChildThread[i].IsFinished())
{
continue;
}
mChildContext[i].MoveNext();
}
bool isDone = base.InternalRun();
if(isDone && mWaitChild)
{
foreach(var t in mChildThread)
{
if(!t.IsFinished())
{
return false;
}
}
}
return isDone;
}
}