В предыдущей статье я привел пример функции-итератора, исполняющейся по событию от DispatcherTimer. Мне стало интересно развить эту идею дальше. Несколько таких функций с независимыми таймерами образуют систему с кооперативной многозадачностью. Далее будем называть такие функции ко-итераторами.

Ко-итераторы выполняются в главном потоке программы, поэтому их общение как между собой,
так и с элементами пользовательского интерфейса, не требует применения средств межпоточного взаимодействия.

Как и 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; } }
  }

Поделиться с друзьями
-->

Комментарии (8)


  1. shai_hulud
    15.11.2016 10:48
    +6

    А чем не устраивает обычные .NET средства, которое уже созданы для решения этой задачи?
    async/await, Task.Delay, IProgress[T]?

    Пример:

    public static Task Progress(IProgress<ProgressInfo> progress) 
    {
         var iteration = 0;
         var delay = TimeSpan.FromSeconds(1);
         while(true)
         {
    		await Task.Delay(delay);
    		progress.Report(new ProgressInfo(iteration, "iteration: " + iteration))
    		iteration++;
         }
    }
    
    public static Task Timeout(this Task anotherTask, TimeSpan timeout) 
    {
    	var timeoutTask = Task.Delay(timeout);
    	if (await Task.WhenAny(anotherTask, timeoutTask) == timeoutTask)
    		throw new TimeoutException();
    }
    


    1. AmirYantimirov
      15.11.2016 10:56
      -5

      Не могу сказать, что не устраивает. Я еще не применял ни обычные средства, ни ко-итераторы для решения реальных задач. Только прикидывал возможности.


      1. fedorro
        15.11.2016 11:43
        +4

        [sarkazm] Отдам даром парашют собственной разработки. Ни разу не прыгал, и в живую парашют не видел, но по моим прикидкам он должен сработать. [/sarkazm]
        А как человек, который часто применяет это для решения реальных задач могу сказать, что async\await\TPL можно приятнее использовать для тех же целей.


    1. lair
      15.11.2016 12:29
      +1

      ...a еще есть прекрасный мир Rx, где такие комбинаторные возможности открываются.


    1. AmirYantimirov
      16.11.2016 06:51
      -2

      Ну что ж, я хотел написать решение с async/await, для сравнения, но обнаружил, что этот функционал доступен только начиная с .NET 4.5. Мы же продолжаем писать под 3.5. И пока все работает, решение о переходе на следующую версию рантайма принято не будет.

      Пойду прыгать с парашютом в продакшен.


      1. lair
        16.11.2016 11:08

        И пока все работает, решение о переходе на следующую версию рантайма принято не будет.

        А оно "всегда" будет работать. Поэтому вы так навсегда и останетесь без async/await, TPL и прочей адекватной конкурентности.


  1. Szer
    15.11.2016 11:10
    +3

    Выглядит как вредный велосипед.
    Особенно дико смотреть на

    IEnumerable<object>
    в языке с прекрасными дженериками.


  1. dymanoid
    16.11.2016 00:53
    +1

    А зачем

    yield break;
    

    в конце методов?