简单的Coroutines实现[0]

目录

协程类

自定义的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())
{
}

运行输出如下

简单的Coroutines实现[0]_第1张图片

第一帧执行到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循环调用。

自定义的IEnumerator

因为过于简单粗暴,所以直接贴代码。

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;
        }
    }
}

拓展-StClusterThread

另外我还写了一个支持子协程的类,有兴趣的可以直接看代码。

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;
    }
}

 

你可能感兴趣的:(C#)