Asíncrono / espera como reemplazo de corutinas
Utilizo iteradores de C # como reemplazo de las corutinas, y ha funcionado muy bien. Quiero cambiar a async / wait porque creo que la sintaxis es más limpia y me da seguridad de tipo.En esta publicación de blog (obsoleta), Jon Skeet muestra una posible forma de implementarlo.
Elegí tomar un camino ligeramente diferente (implementando mi propioSynchronizationContext
y usandoTask.Yield
) Esto funcionó bien.
Entonces me di cuenta de que habría un problema; Actualmente una corutina no tiene que terminar de ejecutarse. Se puede detener con gracia en cualquier punto donde ceda. Podríamos tener un código como este:
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");
}
}
La rutina funciona al realizar un seguimiento de todos los enumeradores en una pila. El compilador de C # genera unDispose
función que se puede invocar para garantizar que el bloque 'finalmente' se invoque correctamente enCoroutineMain
, incluso si la enumeración no ha terminado. De esta manera, podemos detener una corutina con gracia, y aún así asegurarnos de que finalmente se invoquen los bloques, llamandoDispose
en todo elIEnumerator
objetos en la pila. Esto es básicamente un desenrollado manual.
Cuando escribí mi implementación con async / wait, me di cuenta de que perderíamos esta función, a menos que me equivoque. Luego busqué otras soluciones de rutina, y tampoco parece que la versión de Jon Skeet lo maneje de ninguna manera.
La única forma en que puedo pensar en manejar esto sería tener nuestra propia función personalizada 'Rendimiento', que verificaría si la rutina se detuvo, y luego generaría una excepción que indicara esto. Esto se propagaría, ejecutando finalmente bloques, y luego quedaría atrapado en algún lugar cerca de la raíz. Sin embargo, no me parece bonito, ya que el código de terceros podría capturar la excepción.
¿Estoy malinterpretando algo, y es posible hacerlo de una manera más fácil? ¿O tengo que ir por el camino de la excepción para hacer esto?
EDITAR: se ha solicitado más información / código, así que aquí hay algunos. Puedo garantizar que esto se ejecutará en un solo hilo, por lo que no hay hilos involucrados aquí. Nuestra implementación actual de rutina se parece un poco a esto (esto se simplifica, pero funciona en este caso simple):
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();
}
}
Supongamos que tenemos un código como el siguiente:
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");
}
}
Esto se crearía pasando una instancia del enumerador OrbLogic a una Corutina y luego ejecutándola. Esto nos permite marcar la corutina en cada cuadro.Si el jugador mata el orbe, la rutina no termina de ejecutarse; Desechar simplemente se llama en la rutina. SiMoveTo
estaba lógicamente en el bloque 'probar', luego llamando a Dispose en la parte superiorIEnumerator
hará, semánticamente, elfinally
bosquejarMoveTo
ejecutar. Luego después elfinally
Se ejecutará el bloque en OrbLogic. Tenga en cuenta que este es un caso simple y los casos son mucho más complejos.
Estoy luchando por implementar un comportamiento similar en la versión async / await. El código para esta versión se ve así (se omite la comprobación de errores):
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();
}
}
No veo cómo implementar un comportamiento similar a la versión del iterador usando esto. Disculpas de antemano por el largo código!
EDIT 2: El nuevo método parece estar funcionando. Me permite hacer cosas como:
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();
}
Lo que proporciona una forma muy agradable de construir IA para juegos.