Async / Warten als Ersatz für Coroutinen

Ich benutze C # -Iteratoren als Ersatz für Coroutinen, und es hat großartig funktioniert. Ich möchte zu async / await wechseln, da ich denke, dass die Syntax sauberer ist und mir Typensicherheit gibt.In diesem (veralteten) Blogbeitrag zeigt Jon Skeet, wie man es umsetzen kann.

Ich habe mich für einen etwas anderen Weg entschieden (indem ich meinen eigenen implementiert habe)SynchronizationContext und mitTask.Yield). Das hat gut funktioniert.

Dann wurde mir klar, dass es ein Problem geben würde. Derzeit muss eine Coroutine nicht fertig laufen. Es kann an jedem Punkt, an dem es nachgibt, elegant gestoppt werden. Möglicherweise haben wir folgenden Code:

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

Die Coroutine verfolgt alle Enumeratoren in einem Stapel. Der C # -Compiler generiert eineDispose&nbsp;Funktion, die aufgerufen werden kann, um sicherzustellen, dass der 'finally'-Block korrekt aufgerufen wirdCoroutineMain, auch wenn die Aufzählung noch nicht abgeschlossen ist. Auf diese Weise können wir eine Coroutine elegant stoppen und dennoch sicherstellen, dass durch einen Aufruf endgültig Blöcke aufgerufen werdenDispose&nbsp;auf allenIEnumerator&nbsp;Objekte auf dem Stapel. Dies geschieht im Grunde genommen manuell.

Als ich meine Implementierung mit async / await schrieb, wurde mir klar, dass wir diese Funktion verlieren würden, wenn ich mich nicht irre. Ich habe dann nach anderen Coroutine-Lösungen gesucht und es sieht auch nicht so aus, als würde Jon Skeets Version damit umgehen.

Ich kann mir nur vorstellen, dies zu handhaben, indem wir unsere eigene 'Yield'-Funktion haben, die prüft, ob die Coroutine gestoppt wurde, und dann eine Ausnahme auslöst, die dies anzeigt. Dies würde sich ausbreiten, schließlich Blöcke ausführen und dann irgendwo in der Nähe der Wurzel gefangen werden. Ich finde das allerdings nicht hübsch, da Code von Drittanbietern möglicherweise die Ausnahme abfängt.

Verstehe ich etwas falsch und ist dies auf einfachere Weise möglich? Oder muss ich den Ausnahmeweg gehen, um dies zu tun?

BEARBEITEN: Es wurden weitere Informationen / Codes angefordert. Ich kann garantieren, dass dies nur für einen einzelnen Thread ausgeführt wird, sodass hier kein Threading erforderlich ist. Unsere aktuelle Coroutine-Implementierung sieht ungefähr so aus (dies ist vereinfacht, funktioniert aber in diesem einfachen Fall):

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

Angenommen, wir haben folgenden Code:

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

Dies wird erstellt, indem eine Instanz des OrbLogic-Enumerators an eine Coroutine übergeben und anschließend ausgeführt wird. Dies ermöglicht es uns, die Coroutine für jeden Frame anzukreuzen.Wenn der Spieler die Kugel tötet, wird die Coroutine nicht beendet; Entsorgen heißt einfach auf der Coroutine. WennMoveTo&nbsp;befand sich logischerweise im 'try'-Block und rief dann oben Dispose aufIEnumerator&nbsp;wird semantisch machen diefinally&nbsp;Block inMoveTo&nbsp;ausführen. Dann danach diefinally&nbsp;Block in OrbLogic wird ausgeführt. Beachten Sie, dass dies ein einfacher Fall ist und die Fälle viel komplexer sind.

Ich habe Mühe, ein ähnliches Verhalten in der Async / Wartet-Version zu implementieren. Der Code für diese Version sieht folgendermaßen aus (Fehlerprüfung weggelassen):

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

Ich verstehe nicht, wie man ein ähnliches Verhalten wie in der Iterator-Version implementiert. Entschuldigung im Voraus für den langen Code!

EDIT 2: Die neue Methode scheint zu funktionieren. Es erlaubt mir Dinge zu tun wie:

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

Dies bietet eine sehr gute Möglichkeit, KI für Spiele zu erstellen.