28

So in the sad days of C# 4.0, I created the following "WorkflowExecutor" class that allowed asynchronous workflows in the GUI thread by hacking into IEnumerable's "yield return" continuations to wait for observables. So the following code would, at button1Click, just start a simple workflow that updates the text, waits for you to click button2, and loops after 1 second.

public sealed partial class Form1 : Form {
    readonly Subject<Unit> _button2Subject = new Subject<Unit>();
    readonly WorkflowExecutor _workflowExecutor = new WorkflowExecutor();

    public Form1() {
        InitializeComponent();
    }

    IEnumerable<IObservable<Unit>> CreateAsyncHandler() {
        Text = "Initializing";
        var scheduler = new ControlScheduler(this);
        while (true) {
            yield return scheduler.WaitTimer(1000);
            Text = "Waiting for Click";
            yield return _button2Subject;
            Text = "Click Detected!";
            yield return scheduler.WaitTimer(1000);
            Text = "Restarting";
        }
    }

    void button1_Click(object sender, EventArgs e) {
        _workflowExecutor.Run(CreateAsyncHandler());
    }

    void button2_Click(object sender, EventArgs e) {
        _button2Subject.OnNext(Unit.Default);
    }

    void button3_Click(object sender, EventArgs e) {
        _workflowExecutor.Stop();
    }
}

public static class TimerHelper {
    public static IObservable<Unit> WaitTimer(this IScheduler scheduler, double ms) {
        return Observable.Timer(TimeSpan.FromMilliseconds(ms), scheduler).Select(_ => Unit.Default);
    }
}

public sealed class WorkflowExecutor {
    IEnumerator<IObservable<Unit>> _observables;
    IDisposable _subscription;

    public void Run(IEnumerable<IObservable<Unit>> actions) {
        _observables = (actions ?? new IObservable<Unit>[0]).GetEnumerator();
        Continue();
    }

    void Continue() {
        if (_subscription != null) {
            _subscription.Dispose();
        }
        if (_observables.MoveNext()) {
            _subscription = _observables.Current.Subscribe(_ => Continue());
        }
    }

    public void Stop() {
        Run(null);
    }
}

The smart part of the idea, using "yield" continuations to do the asynchronous work, was taken from Daniel Earwicker's AsyncIOPipe idea: http://smellegantcode.wordpress.com/2008/12/05/asynchronous-sockets-with-yield-return-of-lambdas/, then I added the reactive framework on top of it.

Now I'm having trouble rewriting this using the async feature in C# 5.0, but it seems like it should be straightforward thing to do. When I convert the observables to tasks, they only run once and the while loop crashes the 2nd time around. Any help fixing that would be great.

All that said/asked, what does the async/await mechanism give me that the WorkflowExecutor doesn't? Is there anything I can do with async/await that I can't just do (given a similar amount of code) with the WorkflowExecutor?

Dax Fohl
  • 10,654
  • 6
  • 46
  • 90
  • How exactly did you do that conversion to `Task`s? How does the look crash? – svick Apr 24 '12 at 00:46
  • 1
    And `await` has many advantages over this kind of asynchrony, but one of the big differences is that returning from the awaitables. E.g. `string s = await client.DownloadStringAsync(url);`. – svick Apr 24 '12 at 01:30

2 Answers2

39

As James mentioned, you can await an IObservable<T> sequence starting with Rx v2.0 Beta. The behavior is to return the last element (before the OnCompleted), or throw the OnError that was observed. If the sequence contains no elements, you'll get an InvalidOperationException out.

Notice using this, you can get all other desired behaviors:

  • Get the first element by awaiting xs.FirstAsync()
  • Ensure there's only a single value by awaiting xs.SingleAsync()
  • When you're fine with an empty sequence, await xs.DefaultIfEmpty()
  • To get all the elements, await xs.ToArray() or await xs.ToList()

You can do even more fancy things, like computing the result of an aggregation but observe intermediate values by using Do and Scan:

var xs = Observable.Range(0, 10, Scheduler.Default);

var res = xs.Scan((x, y) => x + y)
            .Do(x => { Console.WriteLine("Busy. Current sum is {0}", x); });

Console.WriteLine("Done! The sum is {0}", await res);
Bart De Smet
  • 786
  • 4
  • 3
  • 3
    This is the information I happened to be looking for after being surprised to see in a recent project that awaiting an IObservable built just fine. Thanks for sharing. – jpierson Mar 15 '15 at 16:45
  • 1
    Just want to mention here, since google search leads to this answer, that the new extension `ToTask` also behaves like this. It will specifically do `_tcs.TrySetException(new InvalidOperationException(Strings_Linq.NO_ELEMENTS));`. This isn't obvious, I was chasing around my `.First*` calls for hours before realizing that the observable itself was throwing this in a scenario which was expected by me. – bokibeg Apr 15 '19 at 17:55
33

As you noticed, Task is very much a one-time use thing, as opposed to Observable's "stream of events". A good way of thinking of this (IMHO) is the 2x2 chart on the Rx team's post about 2.0 Beta:

2x2 chart for task vs observable

Depending on circumstance (one-time vs. 'stream' of events), keeping Observable might make more sense.

If you can hop up to the Reactive 2.0 Beta, then you can 'await' observables with that. For instance, my own attempt at an 'async/await' (approximate) version of your code would be:

public sealed partial class Form1 : Form
{
    readonly Subject<Unit> _button2Subject = new Subject<Unit>();

    private bool shouldRun = false;

    public Form1()
    {
        InitializeComponent();
    }

    async Task CreateAsyncHandler()
    {
        Text = "Initializing";
        while (shouldRun)
        {
            await Task.Delay(1000);
            Text = "Waiting for Click";
            await _button2Subject.FirstAsync();
            Text = "Click Detected!";
            await Task.Delay(1000);
            Text = "Restarting";
        }
    }

    async void button1_Click(object sender, EventArgs e)
    {
        shouldRun = true;
        await CreateAsyncHandler();
    }

    void button2_Click(object sender, EventArgs e)
    {
        _button2Subject.OnNext(Unit.Default);
    }

    void button3_Click(object sender, EventArgs e)
    {
        shouldRun = false;
    }
}
James Manning
  • 13,429
  • 2
  • 40
  • 64
  • `Task` is one-time use, but you can `await` things that are not `Task`s. So, it should be possible to create an awaitable that can represent the whole `IObservable`, not just one item. – svick Apr 24 '12 at 06:36
  • That's what I did in the code sample. With Rx 2.0, you can await observables. The default behavior is returning the last element of the observable, which is why it does FirstAsync – James Manning Apr 24 '12 at 10:10