gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 12:30:08 25 4
gpt4 key购买 nike

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

背景

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

•我想将每个AI(例如怪物)作为自己的协程(或嵌套的协程)运行,以便主游戏线程可以每帧(每秒60次)选择“单步执行”部分或全部人工智能取决于其他工作量。

•但是出于可读性和易于编码的目的,我想编写AI代码,使其唯一的线程感知功能是在完成任何重要工作后“屈服”其时间片;并且我希望能够“屈服”中间方法,并在所有本地人完好无损的情况下恢复下一帧(正如您期望的那样。)

•我不想使用IEnumerable <>来获得 yield ,部分是由于丑陋,部分是由于对所报告问题的迷信,尤其是异步和等待看起来更合乎逻辑。

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

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();
}
...

方法

在Windows 2012桌面版VS 2012 Express(.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()。Run()中,然后可以在该类中使用Debugger.Break()(但不能设置断点) 'Post()方法,然后在这里看到异常。但是,单线程同步上下文只是按顺序执行。它无法完成将异常传播回调用者的工作。因此,在完成了整个协调程序序列(及其捕获块)并进行了尘土处理后,异常异常严重。

我尝试了更灵活的方法来派生我自己的SynchronizationContext,该方法的Post()方法立即立即执行给定的Action;这看起来很有希望(如果邪恶,并且对于在该上下文处于 Activity 状态的任何复杂代码毫无疑问会带来可怕的后果?),但这与生成的状态机代码不符:AsyncMethodBuilderCore.ThrowAsync的通用catch处理程序将捕获此尝试并重新抛出线程池!

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

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

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

协调器在执行任何操作(协程或异步继续)之前将其设置为当前上下文,然后再还原之前的上下文。

在协调器的通常队列中执行任何 Action 后,我可以坚持认为它会执行特殊队列中的每个 Action 。这意味着AsyncMethodBuilderCore.ThrowAsync()导致相关延续过早退出后立即引发异常。 (从AsyncMethodBuilderCore抛出的异常中提取原始异常仍然有很多工作要做。)

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

最佳答案

有趣的难题。

问题

如您所指出的,问题是默认情况下,使用void async方法捕获的任何异常都是使用AsyncVoidMethodBuilder.SetException捕获的,然后使用AsyncMethodBuilderCore.ThrowAsync();捕获。麻烦的是,因为一旦存在,异常就会被抛出到另一个线程(从线程池中)。似乎没有任何方法可以覆盖此行为。

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

一个办法

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

这需要对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# - 如何在基于异步/等待的单线程协程实现中捕获异常,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/17054055/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com