异步/等待作为协同程序的替代品

我使用C#迭代器作为协同程序的替代品,它一直很好用。 我想切换到async / await,因为我认为语法更清晰,它给了我类型安全。 在这篇(过时的)博客文章中,Jon Skeet展示了实现它的可能方法 。

我选择采用稍微不同的方式(通过实现我自己的SynchronizationContext并使用Task.Yield )。 这很好用。

然后我意识到会有问题; 目前协程不必完成运行。 它可以在任何产生的点上优雅地停止。 我们可能有这样的代码:

 private IEnumerator Sleep(int milliseconds) { Stopwatch timer = Stopwatch.StartNew(); do { yield return null; } while (timer.ElapsedMilliseconds < milliseconds); } private IEnumerator CoroutineMain() { try { // Do something that runs over several frames yield return Coroutine.Sleep(5000); } finally { Log("Coroutine finished, either after 5 seconds, or because it was stopped"); } } 

协程通过跟踪堆栈中的所有枚举器来工作。 C#编译器生成一个Dispose函数,可以调用它以确保在CoroutineMain正确调用’finally’块,即使枚举未完成。 通过这种方式,我们可以优雅地停止协程,并通过在堆栈上的所有IEnumerator对象上调用Dispose来确保最终调用块。 这基本上是手动展开。

当我用async / await编写我的实现时,我意识到我们会失去这个function,除非我弄错了。 然后我查找了其他协程解决方案,看起来Jon Skeet的版本也不会以任何方式处理它。

我能想到处理这个问题的唯一方法就是拥有我们自己的自定义’Yield’函数,它会检查协程是否被停止,然后引发一个指示这个的exception。 这将传播,执行finally块,然后被捕获到根附近的某处。 我不觉得这很漂亮,因为第三方代码可能会捕获exception。

我误解了什么,这是否可以更容易地做到? 或者我需要以exception方式执行此操作吗?

编辑:已经请求了更多的信息/代码,所以这里有一些。 我可以保证这只会在一个线程上运行,所以这里没有涉及线程。 我们当前的协程实现看起来有点像这样(这是简化的,但它适用于这个简单的情况):

 public sealed class Coroutine : IDisposable { private class RoutineState { public RoutineState(IEnumerator enumerator) { Enumerator = enumerator; } public IEnumerator Enumerator { get; private set; } } private readonly Stack _enumStack = new Stack(); public Coroutine(IEnumerator enumerator) { _enumStack.Push(new RoutineState(enumerator)); } public bool IsDisposed { get; private set; } public void Dispose() { if (IsDisposed) return; while (_enumStack.Count > 0) { DisposeEnumerator(_enumStack.Pop().Enumerator); } IsDisposed = true; } public bool Resume() { while (true) { RoutineState top = _enumStack.Peek(); bool movedNext; try { movedNext = top.Enumerator.MoveNext(); } catch (Exception ex) { // Handle exception thrown by coroutine throw; } if (!movedNext) { // We finished this (sub-)routine, so remove it from the stack _enumStack.Pop(); // Clean up.. DisposeEnumerator(top.Enumerator); if (_enumStack.Count <= 0) { // This was the outer routine, so coroutine is finished. return false; } // Go back and execute the parent. continue; } // We executed a step in this coroutine. Check if a subroutine is supposed to run.. object value = top.Enumerator.Current; IEnumerator newEnum = value as IEnumerator; if (newEnum != null) { // Our current enumerator yielded a new enumerator, which is a subroutine. // Push our new subroutine and run the first iteration immediately RoutineState newState = new RoutineState(newEnum); _enumStack.Push(newState); continue; } // An actual result was yielded, so we've completed an iteration/step. return true; } } private static void DisposeEnumerator(IEnumerator enumerator) { IDisposable disposable = enumerator as IDisposable; if (disposable != null) disposable.Dispose(); } } 

假设我们有如下代码:

 private IEnumerator MoveToPlayer() { try { while (!AtPlayer()) { yield return Sleep(500); // Move towards player twice every second CalculatePosition(); } } finally { Log("MoveTo Finally"); } } private IEnumerator OrbLogic() { try { yield return MoveToPlayer(); yield return MakeExplosion(); } finally { Log("OrbLogic Finally"); } } 

这可以通过将OrbLogic枚举器的实例传递给Coroutine,然后运行它来创建。 这允许我们每帧勾选协程。 如果玩家杀死了球,则协程未完成运行 ; 在协程上简单地调用Dispose。 如果MoveTo逻辑上位于’try’块中,那么在顶层IEnumerator上调用Dispose将在语义上使MoveTofinally块执行。 然后,OrbLogic中的finally块将执行。 请注意,这是一个简单的案例,案例要复杂得多。

我正在努力在async / await版本中实现类似的行为。 此版本的代码如下所示(省略错误检查):

 public class Coroutine { private readonly CoroutineSynchronizationContext _syncContext = new CoroutineSynchronizationContext(); public Coroutine(Action action) { if (action == null) throw new ArgumentNullException("action"); _syncContext.Next = new CoroutineSynchronizationContext.Continuation(state => action(), null); } public bool IsFinished { get { return !_syncContext.Next.HasValue; } } public void Tick() { if (IsFinished) throw new InvalidOperationException("Cannot resume Coroutine that has finished"); SynchronizationContext curContext = SynchronizationContext.Current; try { SynchronizationContext.SetSynchronizationContext(_syncContext); // Next is guaranteed to have value because of the IsFinished check Debug.Assert(_syncContext.Next.HasValue); // Invoke next continuation var next = _syncContext.Next.Value; _syncContext.Next = null; next.Invoke(); } finally { SynchronizationContext.SetSynchronizationContext(curContext); } } } public class CoroutineSynchronizationContext : SynchronizationContext { internal struct Continuation { public Continuation(SendOrPostCallback callback, object state) { Callback = callback; State = state; } public SendOrPostCallback Callback; public object State; public void Invoke() { Callback(State); } } internal Continuation? Next { get; set; } public override void Post(SendOrPostCallback callback, object state) { if (callback == null) throw new ArgumentNullException("callback"); if (Current != this) throw new InvalidOperationException("Cannot Post to CoroutineSynchronizationContext from different thread!"); Next = new Continuation(callback, state); } public override void Send(SendOrPostCallback d, object state) { throw new NotSupportedException(); } public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) { throw new NotSupportedException(); } public override SynchronizationContext CreateCopy() { throw new NotSupportedException(); } } 

我没有看到如何使用它实现与迭代器版本类似的行为。 为漫长的代码提前道歉!

编辑2:新方法似乎正在起作用。 它允许我做的事情:

 private static async Task Test() { // Second resume await Sleep(1000); // Unknown how many resumes } private static async Task Main() { // First resume await Coroutine.Yield(); // Second resume await Test(); } 

这为游戏构建AI提供了一种非常好的方式。

我使用C#迭代器作为协同程序的替代品,它一直很好用。 我想切换到async / await,因为我认为语法更清晰,它给了我类型安全…

IMO,这是一个非常有趣的问题,虽然我需要一段时间才能完全理解它。 也许,您没有提供足够的示例代码来说明这个概念。 一个完整的应用程序会有所帮助,所以我会先尝试填补这个空白。 以下代码说明了我理解的使用模式,如果我错了请纠正我:

 using System; using System.Collections; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication { // https://stackoverflow.com/q/22852251/1768303 public class Program { class Resource : IDisposable { public void Dispose() { Console.WriteLine("Resource.Dispose"); } ~Resource() { Console.WriteLine("~Resource"); } } private IEnumerator Sleep(int milliseconds) { using (var resource = new Resource()) { Stopwatch timer = Stopwatch.StartNew(); do { yield return null; } while (timer.ElapsedMilliseconds < milliseconds); } } void EnumeratorTest() { var enumerator = Sleep(100); enumerator.MoveNext(); Thread.Sleep(500); //while (e.MoveNext()); ((IDisposable)enumerator).Dispose(); } public static void Main(string[] args) { new Program().EnumeratorTest(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); GC.WaitForPendingFinalizers(); Console.ReadLine(); } } } 

这里,因为((IDisposable)enumerator).Dispose()而调用Resource.Dispose 。 如果我们不调用enumerator.Dispose() ,那么我们必须取消注释//while (e.MoveNext()); 并让迭代器优雅地完成,以便正确展开。

现在,我认为使用async/await实现此function的最佳方法是使用自定义awaiter

 using System; using System.Collections; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication { // https://stackoverflow.com/q/22852251/1768303 public class Program { class Resource : IDisposable { public void Dispose() { Console.WriteLine("Resource.Dispose"); } ~Resource() { Console.WriteLine("~Resource"); } } async Task SleepAsync(int milliseconds, Awaiter awaiter) { using (var resource = new Resource()) { Stopwatch timer = Stopwatch.StartNew(); do { await awaiter; } while (timer.ElapsedMilliseconds < milliseconds); } Console.WriteLine("Exit SleepAsync"); } void AwaiterTest() { var awaiter = new Awaiter(); var task = SleepAsync(100, awaiter); awaiter.MoveNext(); Thread.Sleep(500); //while (awaiter.MoveNext()) ; awaiter.Dispose(); task.Dispose(); } public static void Main(string[] args) { new Program().AwaiterTest(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); GC.WaitForPendingFinalizers(); Console.ReadLine(); } // custom awaiter public class Awaiter : System.Runtime.CompilerServices.INotifyCompletion, IDisposable { Action _continuation; readonly CancellationTokenSource _cts = new CancellationTokenSource(); public Awaiter() { Console.WriteLine("Awaiter()"); } ~Awaiter() { Console.WriteLine("~Awaiter()"); } public void Cancel() { _cts.Cancel(); } // let the client observe cancellation public CancellationToken Token { get { return _cts.Token; } } // resume after await, called upon external event public bool MoveNext() { if (_continuation == null) return false; var continuation = _continuation; _continuation = null; continuation(); return _continuation != null; } // custom Awaiter methods public Awaiter GetAwaiter() { return this; } public bool IsCompleted { get { return false; } } public void GetResult() { this.Token.ThrowIfCancellationRequested(); } // INotifyCompletion public void OnCompleted(Action continuation) { _continuation = continuation; } // IDispose public void Dispose() { Console.WriteLine("Awaiter.Dispose()"); if (_continuation != null) { Cancel(); MoveNext(); } } } } } 

在放松的时候,我要求在Awaiter.Dispose内部取消。 Awaiter.Dispose状态机驱动到下一步(如果有待处理的继续)。 这导致观察Awaiter.GetResult内部的取消(由编译器生成的代码调用)。 抛出TaskCanceledException并进一步展开using语句。 因此, Resource得到妥善处理。 最后,任务转换到已取消状态( task.IsCancelled == true )。

IMO,这是一种比在当前线程上安装自定义同步上下文更简单直接的方法。 它可以很容易地适应multithreading( 这里有更多细节)。

这确实应该比IEnumerator / yield更自由。 您可以在coroutine逻辑中使用try/catch ,并且可以直接通过Task对象观察exception,取消和结果。

更新后 ,AFAIK对于迭代器生成的IDispose没有类比,当谈到async状态机时。 当你想取消/解除它时,你真的必须驱动状态机。 如果你想考虑一些疏忽使用try/catch防止取消,我认为你能做的最好的事情就是在Awaiter.Cancel (在MoveNext之后)检查_continuation是否为非null并抛出一个致命的exception- the-band (使用辅助async void方法)。