编程语言
首页 > 编程语言> > c# – 如何在基于异步/等待的单线程协同程序实现中捕获异常

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

作者:互联网

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

背景

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

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

•但是为了易读性和易于编码,我想编写AI代码,使其唯一的线程感知是在完成任何重要工作后“产生”其时间片;而且我希望能够“屈服”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的发行版本需要它.

尽管如此,创建一个运行示例代码的控制台应用程序可以很好地工作,这正是我想要的:单个线程上的协作多线程,等待“yield”,没有IEnumerable的丑陋<>基于协同程序.

现在我编辑示例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);
    }
}

我天真地希望这个例外会被抓住.相反它不是 – 在这个“单线程”协程实现中,它被抛出一个线程池线程,因此没有被捕获.

尝试修复此方法

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

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

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

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

部分“解决方案”,可能不明智?

继续考虑,我有一个部分“解决方案”,但我不确定是什么后果,因为我宁愿在黑暗中钓鱼.

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

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

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

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

解决方法:

有趣的拼图.

问题

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

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

一个办法

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

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

private List<Func<Coordinator, Task>> initialCoroutines = new List<Func<Coordinator, Task>>();
private List<Task> coroutineTasks = new List<Task>();

initialCoroutines最初存储添加到协调器的协同程序,而coroutineTasks存储初始调用initialCoroutines时产生的任务.

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

foreach (var taskFunc in initialCoroutines)
{
    coroutineTasks.Add(taskFunc(this));
}

while (actions.Count > 0)
{
    Task failed = coroutineTasks.FirstOrDefault(t => t.IsFaulted);
    if (failed != null)
    {
        throw failed.Exception;
    }
    actions.Dequeue().Invoke();
}

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

标签:c,coroutine,asynchronous,async-await,game-engine
来源: https://codeday.me/bug/20190703/1369432.html