简单的Coroutines实现[1]

关于具体的实现原理请看上篇简单的Coroutines实现[0]。

这篇主要介绍如何记录并打印协程的调用堆栈。

问题

在实际使用中,我发现一个问题,就是当协程内部调用的函数发生错误时,输出的错误信息阅读起来十分吃力。

还是看上一篇的例子,假设在SubFunction1中发生了错误抛出异常,打印出来的错误信息中只有SubFunction1的信息,而不会包含调用它的SubFunction0和Run。

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

而实际上为了方便Debug,我们希望的调用信息应该是类似

at ThreadB.SubFunction1 [File.cs:linexxx]

at ThreadB.SubFunction0 [File.cs:linexxx]

at ThreadB.Run [File.cs:linexxx]

以便我们更好的判断是何处调用了这个产生错误的函数。

因此在执行协程函数时,我们需要捕获异常并重构调用堆栈信息再抛出新的异常,从而方便我们Debug。

调用栈重构

实际上我们的mContextStack就是协程函数的调用栈,所以首先想到的是通过mContextStack中的信息来重构我们的调用堆栈。

在这篇c# yield关键字原理详解中详细介绍了yield的实质,我们可以知道yield return实际上就是一个状态机。

在yield return返回的IEnumerator值中,存有状态机的state的值(通过反射可以获取,注意state是private的),所以我首先想到的是通过state的值,找到代码文件中对应的yield return关键字的位置,再重建调用信息。

经过一些查找我最终在GitHub上找到了一个协程实现,其中就有如何通过IEnumerator获得调用的文件路径函数名以及类名的实现,具体可以参照 jeansimonet/Coroutines。

但是这里我们不采用这种办法,因为上述的实现需要使用一个第三的Mono库,个人不太喜欢使用非必要的第三方库,所以我们介绍另一种实现,那就是手动记录调用栈,即在每次yield return function()之前,把当前的位置信息保存起来,在需要时把它打印出来即可。完整的实现我已经放到我的Github上。

StackFrame

Stack                   mCallStackFrames;

我在StThread中添加了一个栈用来保存堆栈信息。理论上每个Context对应一个调用它的栈帧,但是由于初次调用的位置是在协程外部,我不打算记录这部分的信息,所以这里mCallStackFrames的数量总是等于mContextStack.Count-1,所以为了避免在使用过程中有人忘记记录调用栈,我在每次有新的Context入栈时,都会检查一次

    protected void ContextStackPush(IEnumerator context)
    {
        mContextStack.Push(context);
#if DEBUG
        Debug.AssertFormat(mContextStack.Count == mCallStackFrames.Count + 1,
                   "Forget TrackStack when yield return? Next Call is {0}", context.ToString());
#endif
    }

同样的我们在出栈是也要把调用它的栈帧信息一同Pop掉,所以

    protected IEnumerator ContextStackPop()
    {
#if DEBUG
        Debug.Assert(mContextStack.Count == mCallStackFrames.Count + 1, "Please use ContextStackPush and ContextStackPop.");
        if(mContextStack.Count > 1)
        {
            mCallStackFrames.Pop();
        }
#endif
        return mContextStack.Pop();
    }

在函数TraceStack,我们通过读取StackFrame中的信息,按照抛出异常是的CallStack的格式重新构建成我们想要的格式。这里需要注意的时我们无法通过method.Name获得原来函数的名字(因为编译器已经把它变成函数状态机了,原来的函数已经不复存在),所以我们要通过DeclaringType用正则表达式从中提取出我们想要的类名和函数名。另外这里唯一的不足是,我们无法获得函数的参数,但只要有调用位置信息我们就可以知道我们具体调用了哪个函数,所以我认为这是可以忍受的。

    public void TraceStack(System.Diagnostics.StackFrame stackFrame)
    {
        var method = stackFrame.GetMethod();
        string className = method.DeclaringType.Name;
        string methodName = method.Name;
        Regex regEx = new Regex(@"(.+)\+\<(.+)\>");
        var match = regEx.Match(method.DeclaringType.ToString());
        if(match.Success)
        {
            className = match.Groups[1].Value;
            methodName = match.Groups[2].Value;
        }

        mCallStackBuilder.Length = 0;
        mCallStackBuilder.AppendFormat
            (
            "at {0}.{1} () [0x0000] in {2}:{3}",
            className,
            methodName,
            stackFrame.GetFileName(),
            stackFrame.GetFileLineNumber()
            );

        mCallStackFrames.Push(mCallStackBuilder.ToString());
    }

最后,在InternalRun中,我们捕获并抛出新的异常,调用栈的信息也会被传入到新异常中。StThreadException的实现比较简单,不详述。

protected virtual bool InternalRun()
    {
        RunningThread = this;
        bool isDone = true;
        while(mContextStack.Count > 0)
        {
            IEnumerator top = mContextStack.Peek();
#if DEBUG
            try
            {
#endif
                while(isDone = !top.MoveNext())
                {
                    //If one stack frame done, we will try to make it run 
                    ContextStackPop();
                    if(mContextStack.Count > 0)
                    {
                        top = mContextStack.Peek();
                    }
                    else
                    {
                        break;
                    }
                }
#if DEBUG
            }
            catch(Exception e)
            {
                throw new StThreadException(e, CallStack);
            }
#endif
... ...
        }

        RunningThread = null;
        return isDone;
    }

一个技巧-如何调用TraceStack

实际上每次yield return前都要用TraceStack记录调用栈既不美观也不环保,我们能做的就是让调用看起来美观一点。

这里我提供我能想到的两种办法。

其一是通过重载操作符的方式来调用TraceStack,具体来说,例如在ThreadB中。我们先在StThread中重载">"操作符

    public static IEnumerator operator >(StThread caller, IEnumerator nextCall)
    {
#if DEBUG
        if(nextCall != null)
        {
            StThread.RunningThread.TraceStack(new System.Diagnostics.StackFrame(1, true));
        }
#endif
        return nextCall;
    }
    public static IEnumerator operator <(StThread a, IEnumerator shift)
    {
        throw new NotSupportedException();
    }

然后在ThreadB中,我们只需要在每次yield return之后加上this > 即可

public class ThreadB : StThread
{
    protected override IEnumerator Run()
    {
        yield return this > SubFunction0();
    }
 
    IEnumerator SubFunction0()
    {
        yield return this > SubFunction1();
    }
 
 
    IEnumerator SubFunction1()
    {
        yield return null;
    }
};

看上去要比直接调用美观一点。

另一种写法是通过扩展方法,

public static class StThreadTraceHelper
{
    public static IEnumerator Run(this IEnumerator nextCall)
    {
#if DEBUG
        if(nextCall != null)
        {
            StThread.RunningThread.TraceStack(new System.Diagnostics.StackFrame(1, true));
        }
#endif
        return nextCall;
    }
}

public class ThreadB : StThread
{
    protected override IEnumerator Run()
    {
        yield return SubFunction0().Run();
    }
 
    IEnumerator SubFunction0()
    {
        yield return SubFunction1().Run();
    }
 
 
    IEnumerator SubFunction1()
    {
        yield return null;
    }
};

当然如果你都不喜欢的话,推荐使用Mono库从Context中得到调用栈帧的信息。

https://github.com/ZealotAlie/StThread

https://github.com/jeansimonet/Coroutines

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