是否有可能使用异步并等待实现仅在一个线程上运行的性能协同器,不要浪费周期(这是游戏代码),并且可以将异常抛回coroutine的调用方(可能是coroutine本身)?
背景
我正在试验用C# coroutine AI代码替换(宠物游戏项目) Lua coroutine AI代码(在LuaInterface中托管)。
·我想将每个AI (比如怪兽)作为自己的协同线(或嵌套的协同线集)运行,这样主游戏线程就可以根据其他工作负载选择“单步”部分或全部人工智能。
·但为了清晰易懂的编码,我想编写AI代码,这样它唯一的线程意识就是在完成任何重要工作后,“交出”它的时间片段;我希望能够“交出”中间方法,并与所有本地用户保持完整的下一个框架(就像你期望的那样,等待)。
·我不想使用IEnumerable<>和收益回报,部分原因是丑陋,部分原因是对报告问题的迷信,尤其是异步和等待看起来更符合逻辑。
从逻辑上讲,主要游戏的伪代码:
void MainGameInit()
{
foreach (monster in Level)
Coroutines.Add(() => ASingleMonstersAI(monster));
}
void MainGameEachFrame()
{
RunVitalUpdatesEachFrame();
while (TimeToSpare())
Coroutines.StepNext() // round robin is fine
Draw();
} 对于大赦国际而言:
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函数如下:
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移植到应用程序上的尝试并不成功。我尝试添加这一个,在Main()开头调用SetSynchronizationContext(),包装整个协调员的创建和调用AsyncPump().Run(),我可以在该类的‘Post()方法中添加Debugger.Break() (但不是断点),并看到异常在这里出现。但是,单线程同步上下文只是串联执行;它不能完成将异常传播回调用方的工作。因此,在完成整个协调程序序列(及其捕获块)并进行除尘之后,异常就会发生。
我尝试了一种更低调的方法,即派生我自己的SynchronizationContext,它的Post()方法只是立即执行给定的操作;这看起来很有希望(如果这是邪恶的,而且无疑对任何调用具有该上下文活动的复杂代码都有可怕的影响?)但是这违反了生成的状态机代码:AsyncMethodBuilderCore.ThrowAsync的一般捕获处理程序捕捉到这个尝试,并将其重新抛到线程池上!
部分“解决方案”,可能是不明智的?
继续思考,我有一个部分的“解决方案”,但我不知道后果是什么,因为我宁愿在黑暗中钓鱼。
我可以定制Jon的协调器来实例化它自己的SynchronizationContext派生类,这个类有一个对协调员本身的引用。当要求所述上下文发送()或Post()回调(例如AsyncMethodBuilderCore.ThrowAsync())时,它反而要求协调器将其添加到一个特殊的操作队列中。
协调程序在执行任何操作(coroutine或异步延续)之前将其设置为当前上下文,并随后恢复以前的上下文。
在执行协调员通常队列中的任何操作之后,我可以坚持它执行特殊队列中的每个操作。这意味着AsyncMethodBuilderCore.ThrowAsync()会在相关的延续过早退出后立即引发异常。(要从AsyncMethodBuilderCore抛出的异常中提取原始异常,还有一些工作要做。)
但是,由于自定义SynchronizationContext的其他方法并没有被覆盖,而且由于我最终对自己正在做的事情缺乏足够的线索,我会认为这会对任何复杂的(尤其是)产生一些(不愉快的)副作用。异步或面向任务,还是真正的多线程?)作为一个理所当然的代码,由协同调用?
发布于 2013-06-12 02:09:23
有趣的谜团。
问题
正如您已经注意到的,问题在于,在默认情况下,使用AsyncVoidMethodBuilder.SetException捕获使用void异步方法时捕获的任何异常,然后使用AsyncMethodBuilderCore.ThrowAsync();。麻烦,因为一旦它在那里,异常就会抛到另一个线程上(从线程池)。无论如何,似乎并不存在任何可以推翻这种行为的方法。
但是,AsyncVoidMethodBuilder是void方法的异步方法生成器。那么Task异步方法呢?这是通过AsyncTaskMethodBuilder处理的。与此构建器的不同之处在于,它不是将其传播到当前同步上下文,而是调用Task.SetException将异常抛出通知用户。
解决方案
知道Task返回异步方法会在返回的任务中存储异常信息,然后我们可以将我们的协同值转换为任务返回方法,并使用每个coroutine的初始调用返回的任务来检查后面的异常。(注意到不需要更改例程,因为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();
}这样,异常就会传播到原始调用方。
https://stackoverflow.com/questions/17054055
复制相似问题