Ответ 1
Я использую итераторы С# в качестве замены сопрограмм, и это было отлично работает. Я хочу переключиться на async/wait, так как я думаю, что синтаксис чище, и это дает мне безопасность типа...
ИМО, это очень интересный вопрос, хотя мне потребовалось некоторое время, чтобы его полностью понять. Возможно, вы не представили достаточного примера кода для иллюстрации концепции. Полное приложение поможет, поэтому я сначала попытаюсь заполнить этот пробел. Следующий код иллюстрирует шаблон использования, как я его понял, пожалуйста, исправьте меня, если я ошибаюсь:
using System;
using System.Collections;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
// https://stackoverflow.com/q/22852251/1768303
public class Program
{
class Resource : IDisposable
{
public void Dispose()
{
Console.WriteLine("Resource.Dispose");
}
~Resource()
{
Console.WriteLine("~Resource");
}
}
private IEnumerator Sleep(int milliseconds)
{
using (var resource = new Resource())
{
Stopwatch timer = Stopwatch.StartNew();
do
{
yield return null;
}
while (timer.ElapsedMilliseconds < milliseconds);
}
}
void EnumeratorTest()
{
var enumerator = Sleep(100);
enumerator.MoveNext();
Thread.Sleep(500);
//while (e.MoveNext());
((IDisposable)enumerator).Dispose();
}
public static void Main(string[] args)
{
new Program().EnumeratorTest();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
Console.ReadLine();
}
}
}
Здесь Resource.Dispose
вызывается из-за ((IDisposable)enumerator).Dispose()
. Если мы не назовем enumerator.Dispose()
, тогда нам придется раскомментировать //while (e.MoveNext());
и пусть итератор закончит изящно, для правильного размотки.
Теперь я считаю, что лучший способ реализовать это с помощью async/await
- использовать пользовательский awaiter
using System;
using System.Collections;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
// https://stackoverflow.com/q/22852251/1768303
public class Program
{
class Resource : IDisposable
{
public void Dispose()
{
Console.WriteLine("Resource.Dispose");
}
~Resource()
{
Console.WriteLine("~Resource");
}
}
async Task SleepAsync(int milliseconds, Awaiter awaiter)
{
using (var resource = new Resource())
{
Stopwatch timer = Stopwatch.StartNew();
do
{
await awaiter;
}
while (timer.ElapsedMilliseconds < milliseconds);
}
Console.WriteLine("Exit SleepAsync");
}
void AwaiterTest()
{
var awaiter = new Awaiter();
var task = SleepAsync(100, awaiter);
awaiter.MoveNext();
Thread.Sleep(500);
//while (awaiter.MoveNext()) ;
awaiter.Dispose();
task.Dispose();
}
public static void Main(string[] args)
{
new Program().AwaiterTest();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
Console.ReadLine();
}
// custom awaiter
public class Awaiter :
System.Runtime.CompilerServices.INotifyCompletion,
IDisposable
{
Action _continuation;
readonly CancellationTokenSource _cts = new CancellationTokenSource();
public Awaiter()
{
Console.WriteLine("Awaiter()");
}
~Awaiter()
{
Console.WriteLine("~Awaiter()");
}
public void Cancel()
{
_cts.Cancel();
}
// let the client observe cancellation
public CancellationToken Token { get { return _cts.Token; } }
// resume after await, called upon external event
public bool MoveNext()
{
if (_continuation == null)
return false;
var continuation = _continuation;
_continuation = null;
continuation();
return _continuation != null;
}
// custom Awaiter methods
public Awaiter GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get { return false; }
}
public void GetResult()
{
this.Token.ThrowIfCancellationRequested();
}
// INotifyCompletion
public void OnCompleted(Action continuation)
{
_continuation = continuation;
}
// IDispose
public void Dispose()
{
Console.WriteLine("Awaiter.Dispose()");
if (_continuation != null)
{
Cancel();
MoveNext();
}
}
}
}
}
Когда придет время расслабиться, я запрошу отмену внутри Awaiter.Dispose
и выведет конечный автомат на следующий шаг (если есть ожидающее продолжение). Это приводит к наблюдению за отменой внутри Awaiter.GetResult
(который вызывается кодом, генерируемым компилятором). Это выдает TaskCanceledException
и далее разматывает оператор using
. Таким образом, Resource
получает надлежащее распоряжение. Наконец, задача переходит в отмененное состояние (task.IsCancelled == true
).
IMO, это более простой и прямой подход, чем установка пользовательского контекста синхронизации в текущем потоке. Он может быть легко адаптирован для многопоточности (более подробно здесь).
Это действительно даст вам больше свободы, чем при использовании IEnumerator
/yield
. Вы можете использовать try/catch
внутри вашей логики coroutine, и вы можете наблюдать за исключениями, отменой и результатом непосредственно через объект Task
.
Обновлено, AFAIK нет аналогии для итератора, сгенерированного IDispose
, когда дело доходит до состояния async
. Вам действительно нужно управлять конечным автоматом до конца, когда вы хотите его отменить/отключить. Если вы хотите объяснить какое-то небрежное использование try/catch
, препятствующее аннулированию, я думаю, что лучшее, что вы могли бы сделать, это проверить, не содержит ли _continuation
внутри Awaiter.Cancel
(после MoveNext
) и выбросить фатальное исключение вне диапазона (используя вспомогательный метод async void
).