Я использую итераторы С# в качестве замены сопрограмм, и он отлично работает. Я хочу переключиться на async/await, поскольку я думаю, что синтаксис чище, и это дает мне безопасность типа. В этом (устаревшем) сообщении в блоге Jon Skeet показывает возможный способ его реализации.
Я решил немного по-другому (путем реализации моего собственного 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");
}
}
Работа с сопрограммой работает, отслеживая все счетчики в стеке. Компилятор С# генерирует функцию Dispose
, которая может быть вызвана, чтобы гарантировать, что блок "finally" корректно вызывается в CoroutineMain
, даже если перечисление не завершено. Таким образом, мы можем прекратить coroutine изящно и все еще гарантировать, что, наконец, вызывается блоки, вызывая Dispose
для всех объектов IEnumerator
в стеке. Это в основном ручная размотка.
Когда я написал свою реализацию с помощью async/await, я понял, что мы потеряем эту функцию, если я не ошибаюсь. Затем я просмотрел другие решения coroutine, и это не похоже на то, что версия Jon Skeet также обрабатывает ее.
Единственный способ, с помощью которого я мог бы справиться с этим, - это создать собственную функцию "Выход", которая проверит, была ли свернута сопрограмма, и затем создайте исключение, которое указало это. Это будет распространяться, выполняя, наконец, блоки, а затем попадаться где-то рядом с корнем. Я не считаю это довольно, хотя, поскольку сторонний код может потенциально заразиться исключением.
Я что-то недопонимаю, и можно ли это сделать проще? Или мне нужно сделать способ исключения для этого?
EDIT: запрашивается дополнительная информация/код, поэтому некоторые из них. Я могу гарантировать, что это будет работать только на одном потоке, поэтому здесь нет потоков. Наша текущая реализация coroutine немного похожа на это (это упрощено, но работает в этом простом случае):
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 в Coroutine и последующего его запуска. Это позволяет нам отмечать сопрограмму каждого кадра. Если игрок убивает шар, сопрограмма не заканчивается; 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();
}
}
Я не вижу, как реализовать подобное поведение версии итератора, используя это. Извините заранее за длинный код!
EDIT 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();
}
Это обеспечивает очень хороший способ создания ИИ для игр.