Async / wait como uma substituição de corotinas
Eu uso os iteradores C # como um substituto para as corotinas, e tem funcionado muito bem. Eu quero mudar para assíncrono / aguardar, pois acho que a sintaxe é mais limpa e me dá segurança de tipo.Nesta postagem (desatualizada) do blog, Jon Skeet mostra uma maneira possível de implementá-la.
Eu escolhi seguir um caminho um pouco diferente (implementando meu próprioSynchronizationContext
e usandoTask.Yield
) Isso funcionou bem.
Então percebi que haveria um problema; atualmente, uma corotina não precisa terminar a execução. Ele pode ser parado normalmente em qualquer ponto em que produza. Podemos ter um 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");
}
}
A corotina funciona mantendo o controle de todos os enumeradores em uma pilha. O compilador C # gera umDispose
função que pode ser chamada para garantir que o bloco 'final' seja corretamente chamado noCoroutineMain
, mesmo se a enumeração não estiver concluída. Dessa forma, podemos interromper uma corotina normalmente e ainda garantir que finalmente os blocos sejam chamados, chamandoDispose
em todo oIEnumerator
objetos na pilha. Isso é basicamente desenrolar manualmente.
Quando escrevi minha implementação com async / wait, percebi que perderíamos esse recurso, a menos que eu estivesse enganado. Procurei então outras soluções de rotina, e também não parece que a versão de Jon Skeet lida com isso.
A única maneira de pensar em lidar com isso seria ter nossa própria função 'Rendimento' personalizada, que verificaria se a corotina estava parada e, em seguida, geraria uma exceção que indicava isso. Isso se propagaria, executando finalmente os blocos e depois seria pego em algum lugar próximo à raiz. Eu não acho isso muito bonito, pois o código de terceiros pode pegar a exceção.
Estou entendendo mal alguma coisa e isso é possível de forma mais fácil? Ou preciso seguir o caminho de exceção para fazer isso?
EDIT: Mais informações / código foram solicitados, então aqui estão algumas. Posso garantir que isso funcionará apenas em um único thread, portanto não há threading envolvido aqui. Nossa implementação atual de rotina parece um pouco com isso (isso é simplificado, mas funciona neste caso simples):
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();
}
}
Suponha que tenhamos código como o seguinte:
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");
}
}
Isso seria criado passando uma instância do enumerador OrbLogic para uma Coroutine e, em seguida, executando-a. Isso nos permite marcar a corotina em cada quadro.Se o jogador mata a esfera, a corotina não termina de correr; O descarte é simplesmente chamado na corotina. E seMoveTo
estava logicamente no bloco 'try' e depois chamava Dispose na parte superiorIEnumerator
semanticamente, fará afinally
bloquearMoveTo
executar. Depois, ofinally
O bloco no OrbLogic será executado. Observe que este é um caso simples e os casos são muito mais complexos.
Estou lutando para implementar um comportamento semelhante na versão assíncrona / aguardada. O código para esta versão é semelhante a este (verificação de erros omitida):
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();
}
}
Não vejo como implementar um comportamento semelhante à versão do iterador usando isso. Pedimos desculpas antecipadamente pelo longo código!
EDIT 2: O novo método parece estar funcionando. Isso me permite fazer coisas 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();
}
O que fornece uma maneira muito agradável de criar IA para jogos.