Async / ожидают как замена сопрограмм

Я использую итераторы C # в качестве замены сопрограмм, и это прекрасно работает. Я хочу переключиться на async / await, так как считаю, что синтаксис более чистый и обеспечивает безопасность типов.В этом (устаревшем) сообщении в блоге Джон Скит показывает возможный способ его реализации.

Я решил пойти немного другим путем (реализуя свой собственный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 функция, которая может быть вызвана, чтобы гарантировать, что блок 'finally' правильно вызывается вCoroutineMainдаже если перечисление не закончено. Таким образом, мы можем изящно остановить сопрограмму и при этом убедиться, что наконец-то блокируются, вызываяDispose на всехIEnumerator объекты в стеке. Это в основном раскручивание вручную.

Когда я написал свою реализацию с помощью async / await, я понял, что мы потеряем эту функцию, если я не ошибаюсь. Затем я посмотрел другие сопрограммные решения, и похоже, что версия Джона Скита не справляется с этим.

Единственный способ справиться с этим, который я могу придумать, - это иметь нашу собственную функцию 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 в сопрограмму, а затем запустив его. Это позволяет нам отмечать сопрограмму в каждом кадре.Если игрок убивает шар, сопрограмма не заканчивает работу; Dispose просто вызывается на сопрограмму. ЕслиMoveTo логически находился в блоке try, затем вызывал Dispose наверхуIEnumerator будет семантически делатьfinally блокировать вMoveTo выполнить. Затем послеfinally блок в OrbLogic будет выполняться. Обратите внимание, что это простой случай, и случаи гораздо сложнее.

Я изо всех сил пытаюсь реализовать подобное поведение в версии 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();
}

Который обеспечивает очень хороший способ создания ИИ для игр.

Ответы на вопрос(1)

Ваш ответ на вопрос