В предыдущей статье я привел пример функции-итератора, исполняющейся по событию от DispatcherTimer. Мне стало интересно развить эту идею дальше. Несколько таких функций с независимыми таймерами образуют систему с кооперативной многозадачностью. Далее будем называть такие функции ко-итераторами.
Ко-итераторы выполняются в главном потоке программы, поэтому их общение как между собой,
так и с элементами пользовательского интерфейса, не требует применения средств межпоточного взаимодействия.
Как и async/await, ко-итераторы позволяют в императивном стиле описывать логику последовательного выполнения действий с ожиданием, но отличаются тем, что допускают широкую модификацию своего поведения с помощью декораторов, которые вступают в действие не только до- и после вызова, но и на каждой итерации.
Возьмем традиционную задачу долго выполняемого действия с отображением хода выполнения и возможностью прерывания. Стандартным решением будет такой метод:
Мы же доверим прерывание вызывающему коду, а информацию о прогрессе будем возвращать, как результат итерации.
Рассмотрим вызов ко-итератора из ко-итератора.
Примем соглашение, что в таких случаях будем возвращать непосредственно итератор:
А о разворачивании позаботится следующий метод:
Теперь можно написать несколько декораторов.
Выполнить молча:
Выполнить с таймаутом:
Выполнять реже/чаще:
Ждать условия:
В заключение, код объекта, исполняющего вышеописанные ко-итераторы,
Ко-итераторы выполняются в главном потоке программы, поэтому их общение как между собой,
так и с элементами пользовательского интерфейса, не требует применения средств межпоточного взаимодействия.
Как и async/await, ко-итераторы позволяют в императивном стиле описывать логику последовательного выполнения действий с ожиданием, но отличаются тем, что допускают широкую модификацию своего поведения с помощью декораторов, которые вступают в действие не только до- и после вызова, но и на каждой итерации.
Возьмем традиционную задачу долго выполняемого действия с отображением хода выполнения и возможностью прерывания. Стандартным решением будет такой метод:
public struct ProgressInfo {
public string Message;
public int Current, Total;
}
async Task Foo(IProgress<ProgressInfo> progress, CancellationToken ct)
Мы же доверим прерывание вызывающему коду, а информацию о прогрессе будем возвращать, как результат итерации.
public static IEnumerable<object> Process<T>(this ICollection<T> collection, Action<T> action) {
ProgressInfo info = new ProgressInfo() { Total = collection.Count };
foreach (var item in collection) {
action(item);
info.Current++;
info.Message = string.Format("Processed {0:d} from {1:d}", info.Current, info.Total);
yield return info;
}
yield break;
}
Рассмотрим вызов ко-итератора из ко-итератора.
IEnumerable<object> Foo() { yield break; }
IEnumerable<object> Bar() { yield break; }
IEnumerable<object> Baz() {
foreach (var a in Foo()) yield return a;
foreach (var b in Bar()) yield return b;
yield break;
}
Примем соглашение, что в таких случаях будем возвращать непосредственно итератор:
IEnumerable<object> Baz() {
yield return Foo();
yield return Bar();
yield break;
}
А о разворачивании позаботится следующий метод:
public static IEnumerable<object> Flat(this IEnumerable<object> cot) {
foreach (var a in cot) {
var ea = a as IEnumerable<object>;
if (ea==null) yield return a;
else {
foreach (var b in ea.Flat()) yield return b;
}
}
yield break;
}
Теперь можно написать несколько декораторов.
Выполнить молча:
public static void Execute(this IEnumerable<object> cot) {
foreach (var a in cot.Flat()) { }
}
Выполнить с таймаутом:
public class ValueOf<T> {
public T Value;
}
public static IEnumerable<object> Timeout(this IEnumerable<object> cot,
TimeSpan duration, ValueOf<bool> timeout) {
var limit = DateTimeOffset.Now+duration;
foreach (var a in cot.Flat()) {
if (DateTimeOffset.Now>limit) {
timeout.Value = true;
break;
}
yield return a;
}
yield break;
}
Выполнять реже/чаще:
public static IEnumerable<object> Rate(this IEnumerable<object> cot, double rate) {
double summ = 0.001;
foreach (var a in cot.Flat()) {
summ += 1.0;
while (summ>rate) { summ -= rate; yield return a; }
}
yield break;
}
Ждать условия:
public static IEnumerable<object> Wait(Func<bool> condition) {
while (!condition()) yield return null;
yield break;
}
В заключение, код объекта, исполняющего вышеописанные ко-итераторы,
который не столь интересен.
public class TimerThread {
bool _IsCancelled, _IsCompleted;
DispatcherTimer Timer;
IEnumerator<object> Enumerator;
public event Action<ProgressInfo> Progress;
public TimerThread(IEnumerable<object> cot, double interval) {
Enumerator = cot.Flat().GetEnumerator();
Timer = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(interval) };
Timer.Tick += Timer_Tick;
}
void Timer_Tick(object sender, EventArgs ea) {
if (!Enumerator.MoveNext()) {
_IsCompleted = true;
Timer.IsEnabled = false;
} else if (Enumerator.Current is ProgressInfo) {
if (Progress!=null) Progress((ProgressInfo)Enumerator.Current);
}
}
public bool IsEnabled {
get { return Timer.IsEnabled; }
set {
if (_IsCancelled || _IsCompleted) return;
Timer.IsEnabled = value;
}
}
public bool IsCancelled {
get { return _IsCancelled; }
set {
if (!value) return;
_IsCancelled = true;
Timer.IsEnabled = false;
}
}
public bool IsCompleted { get { return _IsCompleted; } }
}
Поделиться с друзьями
shai_hulud
А чем не устраивает обычные .NET средства, которое уже созданы для решения этой задачи?
async/await, Task.Delay, IProgress[T]?
Пример:
AmirYantimirov
Не могу сказать, что не устраивает. Я еще не применял ни обычные средства, ни ко-итераторы для решения реальных задач. Только прикидывал возможности.
fedorro
[sarkazm] Отдам даром парашют собственной разработки. Ни разу не прыгал, и в живую парашют не видел, но по моим прикидкам он должен сработать. [/sarkazm]
А как человек, который часто применяет это для решения реальных задач могу сказать, что async\await\TPL можно приятнее использовать для тех же целей.
lair
...a еще есть прекрасный мир Rx, где такие комбинаторные возможности открываются.
AmirYantimirov
Ну что ж, я хотел написать решение с async/await, для сравнения, но обнаружил, что этот функционал доступен только начиная с .NET 4.5. Мы же продолжаем писать под 3.5. И пока все работает, решение о переходе на следующую версию рантайма принято не будет.
Пойду прыгать с парашютом в продакшен.
lair
А оно "всегда" будет работать. Поэтому вы так навсегда и останетесь без
async
/await
, TPL и прочей адекватной конкурентности.