异步/等待作为协程的替代
我使用C#迭代器作为协程的替代品,并且它一直在努力工作。 我想切换到异步/等待,因为我认为语法更清晰,它给我类型安全。 在这个(过时的)博客文章中,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
来确保最终调用块。 这基本上是手动展开。
当我用异步/等待写我的实现时,我意识到我们会失去这个功能,除非我错了。 然后,我查找了其他协同程序解决方案,并且看起来Jon Skeet的版本并不以任何方式处理它。
我能想到处理这个问题的唯一方法就是拥有我们自己定制的“Yield”函数,它将检查协程是否已停止,然后引发一个表示此情况的异常。 这会传播起来,最后执行块,然后被抓到靠近根的地方。 虽然我不觉得这很漂亮,因为第三方代码可能会发现异常。
我是否误会了某些东西,这是否可以以更简单的方式做到? 或者我需要采取例外的方式来做到这一点?
编辑:更多信息/代码已被要求,所以这里有一些。 我可以保证这只会在一个线程上运行,所以这里不涉及线程。 我们目前的协程实现看起来有点像这样(这是简化的,但它适用于这种简单的情况):
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枚举器的实例传递给协程来创建,然后运行它。 这使我们能够在每一帧中勾选协程。 如果玩家杀死球体,那么协程并未完成运行 ; 处理只是在协程上调用。 如果MoveTo
在逻辑上位于'try'块中,那么调用顶部IEnumerator
上的Dispose将在语义上使MoveTo
的finally
块执行。 之后,OrbLogic中的finally
块将执行。 请注意,这是一个简单的案例,案例要复杂得多。
我努力在异步/等待版本中实现类似的行为。 此版本的代码如下所示(省略错误检查):
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#迭代器作为协程的替代品,并且它一直在努力工作。 我想切换到异步/等待,因为我认为语法更清晰,它给我类型安全...
国际海事组织,这是一个非常有趣的问题,尽管我花了一段时间才完全理解它。 也许,你没有提供足够的示例代码来说明这个概念。 一个完整的应用程序会有所帮助,所以我会尽量填补这个空白。 下面的代码说明了我理解的使用模式,如果我错了,请纠正我的错误:
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)enumerator).Dispose()
被调用。 如果我们不调用enumerator.Dispose()
,那么我们将不得不取消注释//while (e.MoveNext());
并让迭代器优雅地完成,以便正确地展开。
现在,我认为用async/await
实现这个最好的方法是使用自定义的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
)。
国际海事组织,这是比在当前线程上安装自定义同步上下文更简单直接的方法。 它可以很容易地适应多线程(这里有更多的细节)。
这确实会给你比IEnumerator
/ yield
更多的自由。 你可以在你的协同逻辑中使用try/catch
,你可以直接通过Task
对象观察异常,取消和结果。
更新了 AFAIK,当涉及async
状态机时,对于迭代器生成的IDispose
没有类比。 当你想取消/放开它时,你真的必须驱动状态机结束。 如果你想解释一些使用try/catch
防止取消的疏忽,我认为你可以做的最好的做法是检查Awaiter.Cancel
_continuation
是否为非空(在MoveNext
之后),并且抛出一个致命的异常(out-of- (使用助手async void
方法)。