C# · 12月 31, 2021

c# – Async /等待替代协同程序

我使用C#迭代器作为协同程序的替代,它一直运行良好.我想切换到异步/等待,因为我认为语法更干净,它给我类型安全.
In this (outdated) blog post,Jon Skeet shows a possible way to implement it.

我选择了稍微不同的方式(通过实现我自己的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来确保finally块被调用.这基本上是手动放卷.

当我用异步/等待编写我的实现时,我意识到我们会失去这个功能,除非我错了.然后,我查找了其他协同解决方案,并且它看起来不像Jon Skeet的版本以任何方式处理它.

我可以想到的唯一办法就是拥有自己的定制’Yield’功能,这将检查协同程序是否停止,然后引发一个异常指示.这会传播起来,执行finally块,然后被抓到根部附近的某个地方.我没有找到这个,但第三方代码可能会捕获异常.

我是否误解了某些事情,这样做可能会更容易吗?或者我需要采取例外的方式来做到这一点吗?

编辑:更多信息/代码已被要求,所以这里有一些.我可以保证这只是在一个线程上运行,所以这里没有线程.
我们目前的协同实现看起来有点像这样(这是简化的,但它在这种简单的情况下工作):

public sealed class Coroutine : IDisposable{ private class RoutineState { public RoutineState(IEnumerator enumerator) { Enumerator = enumerator; } public IEnumerator Enumerator { get; private set; } } private readonly Stack<RoutineState> _enumStack = new Stack<RoutineState>(); 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,然后运行它来创建.这允许我们每帧都勾选协程.如果玩家杀死了球,那么协奏曲没有完成;处理只是在协同程序中调用.如果MoveTo逻辑上处于“try”块,则在顶层IEnumerator上调用Dispose将在语法上使MoveTo中的finally块执行.然后,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提供了非常好的方法.

解决方法

I use C# iterators as a replacement for coroutines,and it has been
working great. I want to switch to async/await as I think the Syntax
is cleaner and it gives me type safety…

海事组织,这是一个非常有趣的问题,尽管我花了一段时间来充分了解这一点.也许,您没有提供足够的示例代码来说明这个概念.一个完整的应用程序将有所帮助,所以我将尝试填补这个空白.以下代码说明了我理解的使用模式,如果我错了,请更正我:

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

在这里,Resource.Dispose被调用,因为((IDisposable)枚举器).Dispose().如果我们不调用enumerator.Dispose(),那么我们将不得不取消注释// while(e.MoveNext());并让迭代器优雅地完成,以便适当的展开.

现在,我认为用异步/等待实现的最好方法是使用custom 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,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内取消订单.将状态机移动到下一步(如果有待处理的延续).这导致观察Awaiter.GetResult(由编译器生成的代码调用)中的取消.这会抛出TaskCanceledException,并进一步展开using语句.所以资源得到妥善处理.最后,任务转换到取消状态(task.IsCancelled == true).

IMO,这是比当前线程安装自定义同步上下文更简单直接的方法.它可以轻松适应多线程(更多细节here).

这应该比IEnumerator / yield有更多的自由.您可以在协同逻辑中使用try / catch,您可以通过Task对象直接观察异常,取消和结果.

更新,AFAIK在迭代器生成的IDispose中没有类推,当涉及到异步状态机时.当你想取消/解开它时,你真的要驱动状态机结束.如果你想考虑一些疏忽使用try / catch来阻止取消,我认为最好的方法是检查Awaiter.Cancel(MoveNext之后)中的_continuation是否为非空,并抛出致命异常out-of-the-band(使用助手async void方法).