首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >如何在基于异步/等待的单线程协同实现中捕获异常

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

Stack Overflow用户
提问于 2013-06-11 21:22:24
回答 1查看 976关注 0票数 4

是否有可能使用异步并等待实现仅在一个线程上运行的性能协同器,不要浪费周期(这是游戏代码),并且可以将异常抛回coroutine的调用方(可能是coroutine本身)?

背景

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

·我想将每个AI (比如怪兽)作为自己的协同线(或嵌套的协同线集)运行,这样主游戏线程就可以根据其他工作负载选择“单步”部分或全部人工智能。

·但为了清晰易懂的编码,我想编写AI代码,这样它唯一的线程意识就是在完成任何重要工作后,“交出”它的时间片段;我希望能够“交出”中间方法,并与所有本地用户保持完整的下一个框架(就像你期望的那样,等待)。

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

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

代码语言:javascript
复制
void MainGameInit()
{
    foreach (monster in Level)
        Coroutines.Add(() => ASingleMonstersAI(monster));
}

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

对于大赦国际而言:

代码语言:javascript
复制
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();
}
...

The Approach

在VS 2012 (.NET 4.5)中,我试图逐字使用Jon出色的第13部分:第一次看带有异步的协同中的示例代码,这让我大开眼界。

这个来源是可用的通过这个链接。不使用提供的AsyncVoidMethodBuilder.cs,因为它与mscorlib中的发布版本(这可能是问题的一部分)相冲突。我必须将提供的协调器类标记为实现System.Runtime.CompilerServices.INotifyCompletion,因为.NET 4.5的发行版需要这样做。

尽管如此,创建一个运行示例代码的控制台应用程序工作得很好,也正是我想要的:在一个线程上协同多线程,等待作为“屈服”,而不存在基于IEnumerable<>的协同机制的丑陋之处。

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

代码语言:javascript
复制
private static async void FirstCoroutine(Coordinator coordinator) 
{ 
    await coordinator;
    throw new InvalidOperationException("First coroutine failed.");
}

并按以下方式编辑Main():

代码语言:javascript
复制
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移植到应用程序上的尝试并不成功。我尝试添加这一个,在Main()开头调用SetSynchronizationContext(),包装整个协调员的创建和调用AsyncPump().Run(),我可以在该类的‘Post()方法中添加Debugger.Break() (但不是断点),并看到异常在这里出现。但是,单线程同步上下文只是串联执行;它不能完成将异常传播回调用方的工作。因此,在完成整个协调程序序列(及其捕获块)并进行除尘之后,异常就会发生。

我尝试了一种更低调的方法,即派生我自己的SynchronizationContext,它的Post()方法只是立即执行给定的操作;这看起来很有希望(如果这是邪恶的,而且无疑对任何调用具有该上下文活动的复杂代码都有可怕的影响?)但是这违反了生成的状态机代码:AsyncMethodBuilderCore.ThrowAsync的一般捕获处理程序捕捉到这个尝试,并将其重新抛到线程池上!

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

继续思考,我有一个部分的“解决方案”,但我不知道后果是什么,因为我宁愿在黑暗中钓鱼。

我可以定制Jon的协调器来实例化它自己的SynchronizationContext派生类,这个类有一个对协调员本身的引用。当要求所述上下文发送()或Post()回调(例如AsyncMethodBuilderCore.ThrowAsync())时,它反而要求协调器将其添加到一个特殊的操作队列中。

协调程序在执行任何操作(coroutine或异步延续)之前将其设置为当前上下文,并随后恢复以前的上下文。

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

但是,由于自定义SynchronizationContext的其他方法并没有被覆盖,而且由于我最终对自己正在做的事情缺乏足够的线索,我会认为这会对任何复杂的(尤其是)产生一些(不愉快的)副作用。异步或面向任务,还是真正的多线程?)作为一个理所当然的代码,由协同调用?

EN

回答 1

Stack Overflow用户

发布于 2013-06-12 02:09:23

有趣的谜团。

问题

正如您已经注意到的,问题在于,在默认情况下,使用AsyncVoidMethodBuilder.SetException捕获使用void异步方法时捕获的任何异常,然后使用AsyncMethodBuilderCore.ThrowAsync();。麻烦,因为一旦它在那里,异常就会抛到另一个线程上(从线程池)。无论如何,似乎并不存在任何可以推翻这种行为的方法。

但是,AsyncVoidMethodBuildervoid方法的异步方法生成器。那么Task异步方法呢?这是通过AsyncTaskMethodBuilder处理的。与此构建器的不同之处在于,它不是将其传播到当前同步上下文,而是调用Task.SetException将异常抛出通知用户。

解决方案

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

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

代码语言:javascript
复制
private List<Func<Coordinator, Task>> initialCoroutines = new List<Func<Coordinator, Task>>();
private List<Task> coroutineTasks = new List<Task>();

initialCoroutines存储最初添加到协调器中的协同线,而coroutineTasks存储最初调用initialCoroutines所产生的任务。

然后调整Start()例程以运行新例程,存储结果,然后检查每个新操作之间的任务结果:

代码语言:javascript
复制
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();
}

这样,异常就会传播到原始调用方。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/17054055

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档