如何在基于异步/等待的单线程协程实现中捕获异常

时间:2013-06-11 21:22:24

标签: c# asynchronous game-engine async-await coroutine

是否可以使用async并等待高雅安全地实现高性能协程,这些协同程序仅在一个线程上运行,不会浪费周期(这是游戏代码)并且可以将异常抛回协同程序的调用者(这可能是一个协程本身)?

背景

我尝试用C#coroutine AI代码替换(宠物游戏项目)Lua coroutine AI代码(通过LuaInterface托管在C#中)。

•我想将每个AI(怪物,比如说)作为自己的协程(或嵌套的协程集)运行,这样主游戏线程可以每帧(每秒60次)可以选择"单步骤"部分或全部AI依赖于其他工作量。

•但是为了易读性和编码的简易性,我想编写AI代码,使其唯一的线程感知是" yield"做任何重要工作后的时间片;我希望能够"屈服" mid方法和恢复下一帧,所有当地人等完好无损(正如你期待的那样等待。)

•我不想使用IEnumerable<>和收益率的回报,部分原因在于丑陋,部分原因是由于对报告的问题的迷信,尤其是异步和等待看起来更合乎逻辑的适合。

从逻辑上讲,主游戏的伪代码为:

void MainGameInit()
{
    foreach (monster in Level)
        Coroutines.Add(() => ASingleMonstersAI(monster));
}

void MainGameEachFrame()
{        
     RunVitalUpdatesEachFrame();
     while (TimeToSpare())
          Coroutines.StepNext() // round robin is fine
     Draw();
}                

和AI:

void ASingleMonstersAI(Monster monster)
{
     while (true)
     {
           DoSomeWork(monster);
           <yield to next frame>
           DoSomeMoreWork(monster);
           <yield to next frame>
           ...
     }
}

void DoSomeWork(Monster monster)
{
    while (SomeCondition())
    {
        DoSomethingQuick();
        DoSomethingSlow();
        <yield to next frame>    
    }
    DoSomethingElse();
}
...

方法

使用VS 2012 Express for Windows桌面(.NET 4.5),我试图从Jon Skeet的优秀Eduasync part 13: first look at coroutines with async中逐字使用示例代码,这非常令人大开眼界。

该来源可用via this link。不使用提供的AsyncVoidMethodBuilder.cs,因为它与mscorlib中的发行版本冲突(这可能是问题的一部分)。我必须将提供的Coordinator类标记为实现System.Runtime.CompilerServices.INotifyCompletion,因为.NET 4.5的发行版需要这样做。

尽管如此,创建一个运行示例代码的控制台应用程序可以很好地工作,这正是我想要的:在单个线程上的合作多线程,等待&#34; yield&#34;,没有IEnumerable的丑陋&lt;&gt;基于协同程序。

现在我按如下方式编辑示例FirstCoroutine函数:

private static async void FirstCoroutine(Coordinator coordinator) 
{ 
    await coordinator;
    throw new InvalidOperationException("First coroutine failed.");
}

按如下方式编辑Main():

private static void Main(string[] args) 
{ 
    var coordinator = new Coordinator {  
        FirstCoroutine, 
        SecondCoroutine, 
        ThirdCoroutine 
    }; 
    try
    {
        coordinator.Start(); 
    }
    catch (Exception ex)
    {
         Console.WriteLine("*** Exception caught: {0}", ex);
    }
}

我天真地希望能抓住这个例外。相反,它不是 - 在这个&#34;单线程&#34;协程实现,它被抛出一个线程池线程,因此没有被捕获。

尝试修复此方法

通过阅读我理解部分问题。我收集的控制台应用程序缺少SynchronizationContext。我还认为,在某种意义上,异步空洞并不是为了传播结果,虽然我不知道该怎么做,也不知道如何在单线程实现中添加任务。

我可以从编译器生成的FirstCoroutine状态机代码中看到,通过其MoveNext()实现,任何异常都传递给AsyncVoidMethodBuilder.SetException(),它发现缺少同步上下文并调用最终的ThrowAsync()正如我所见,线程池线程。

然而,我试图天真地将SynchronisationContext移植到应用程序上的尝试并不成功。我尝试添加this one,在Main()的开头调用SetSynchronizationContext(),并包装整个Coordinator创建并调用AsyncPump()。运行(),我可以Debugger.Break()(但不是断点) )在那个班级&#39; Post()方法,看看异常使它在这里。但是,单线程同步上下文只是串行执行;它无法将异常传播回调用者。因此,在整个协调器序列(及其捕获块)完成并且灰尘化之后异常会上升。

我尝试了更加狡猾的方法来派生我自己的SynchronizationContext,其Post()方法只是立即执行给定的Action;这看起来很有希望(如果对于任何使用该上下文激活的任何复杂代码都会产生可怕的后果吗?)但是这会与生成的状态机代码相冲突:AsyncMethodBuilderCore.ThrowAsync的通用catch处理程序捕获此尝试并重新抛出线程池!

部分&#34;解决方案&#34;,可能不明智?

继续仔细考虑,我有一个部分解决方案&#34;但是我不确定这些后果是什么,因为我宁愿在黑暗中钓鱼。

我可以自定义Jon Skeet的协调器来实例化自己的SynchronizationContext派生类,该类具有对协调器本身的引用。当所述上下文被要求Send()或Post()回调(例如通过AsyncMethodBuilderCore.ThrowAsync())时,它会要求协调器将其添加到特殊的Actions队列中。

协调器在执行任何Action(协程或异步延续)之前将其设置为当前上下文,然后恢复之前的上下文。

在协调器的常规队列中执行任何操作后,我可以坚持执行特殊队列中的每个操作。这意味着AsyncMethodBuilderCore.ThrowAsync()会在相关延续过早退出后立即引发异常。 (还有一些钓鱼方法可以从AsyncMethodBuilderCore抛出的异常中提取原始异常。)

然而,由于自定义的SynchronizationContext的其他方法没有被覆盖,并且因为我最终对我正在做的事情缺乏正确的线索,我认为这会有一些(不愉快的)任何复杂的副作用(特别是async或面向任务,或真正的多线程?)代码当然是由coroutines调用的吗?

1 个答案:

答案 0 :(得分:2)

有趣的谜题。

问题

正如您所指出的,问题是默认情况下使用{1}}捕获使用void异步方法时捕获的任何异常,然后使用AsyncVoidMethodBuilder.SetException。麻烦,因为一旦它存在,异常将被抛出另一个线程(来自线程池)。似乎无论如何都不能覆盖这种行为。

但是,AsyncMethodBuilderCore.ThrowAsync();AsyncVoidMethodBuilder方法的异步方法构建器。那个void异步方法怎么样?这是通过Task处理的。与此构建器的不同之处在于,它不是将其传播到当前同步上下文,而是调用AsyncTaskMethodBuilder来通知用户该任务引发了异常。

解决方案

知道Task.SetException返回的异步方法在返回的任务中存储异常信息,然后我们可以将协程转换为task-returning-method,并使用从每个协程的初始调用返回的任务来检查异常稍后的。 (注意,无需更改例程,因为void / Task返回的异步方法完全相同)。

这需要对Task类进行一些更改。首先,我们添加两个新字段:

Coordinator

private List<Func<Coordinator, Task>> initialCoroutines = new List<Func<Coordinator, Task>>(); private List<Task> coroutineTasks = new List<Task>(); 最初存储添加到协调器的协程,而initialCoroutines存储最初调用coroutineTasks时产生的任务。

然后我们的Start()例程适用于运行新例程,存储结果,然后检查每个新操作之间的任务结果:

initialCoroutines

然后,异常会传播到原始调用者。