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();
}
Который обеспечивает очень хороший способ создания ИИ для игр.