1

I have tried to write console observable as in the example below, but it doesn't work. There are some issues with subscriptions. How to solve these issues?

static class Program
{
    static async Task Main(string[] args)
    {
        // var observable = Observable.Interval(TimeSpan.FromMilliseconds(1000)).Publish().RefCount(); // works
        // var observable = FromConsole().Publish().RefCount(); // doesn't work
        var observable = FromConsole(); // doesn't work
        observable.Subscribe(Console.WriteLine);
        await Task.Delay(1500);
        observable.Subscribe(Console.WriteLine);
        await new TaskCompletionSource().Task;
    }

    static IObservable<string> FromConsole()
    {
        return Observable.Create<string>(async observer =>
        {
            while (true)
            {
                observer.OnNext(Console.ReadLine());
            }
        });
    }
}

If I used Observable.Interval, it subscribes two times and I have two outputs for one input. If I used any version of FromConsole, I have one subscription and a blocked thread.

dgzargo
  • 175
  • 1
  • 8

3 Answers3

5

To start with, it is usually best to avoid using Observable.Create to create observables - it's certainly there for that purpose, but it can create observables that don't behave like you think they should because of their blocking nature. As you've discovered!

Instead, when possible, use the built-in operators to create observables. And that can be done in this case.

My version of FromConsole is this:

static IObservable<string> FromConsole() =>
    Observable
        .Defer(() =>
            Observable
                .Start(() => Console.ReadLine()))
        .Repeat();

Observable.Start effectively is like Task.Run for observables. It calls Console.ReadLine() for us without blocking.

The Observable.Defer/Repeat pair repeatedly calls Observable.Start(() => Console.ReadLine()). Without the Defer it would just call Observable.Start and repeatedly return the one string forever.

That solves that.

Now, the second issue is that you want to see the value from the Console.ReadLine() output by both subscriptions to the FromConsole() observable.

Due to the way Console.ReadLine works, you are getting values from each subscription, but only one at a time. Try this code:

static async Task Main(string[] args)
{
    var observable = FromConsole();
    observable.Select(x => $"1:{x}").Subscribe(Console.WriteLine);
    observable.Select(x => $"2:{x}").Subscribe(Console.WriteLine);
    await new TaskCompletionSource<int>().Task;
}

static IObservable<string> FromConsole() =>
    Observable
        .Defer(() =>
            Observable
                .Start(() => Console.ReadLine()))
        .Repeat();
        

When I run that I get this kind of output:

1:ddfd
2:dfff
1:dfsdfs
2:sdffdfd
1:sdfsdfsdf

The reason for this is that each subscription starts up a fresh subscription to FromConsole. So you have two calls to Console.ReadLine() they effectively queue and each one only gets each alternate input. Hence the alternation between 1 & 2.

So, to solve this you simply need the .Publish().RefCount() operator pair.

Try this:

static async Task Main(string[] args)
{
    var observable = FromConsole().Publish().RefCount();
    observable.Select(x => $"1:{x}").Subscribe(Console.WriteLine);
    observable.Select(x => $"2:{x}").Subscribe(Console.WriteLine);
    await new TaskCompletionSource<int>().Task;
}

static IObservable<string> FromConsole() =>
    Observable
        .Defer(() =>
            Observable
                .Start(() => Console.ReadLine()))
        .Repeat();
        

I now get:

1:Hello
2:Hello
1:World
2:World

In a nutshell, it's the combination of the non-blocking FromConsole observable and the use of .Publish().RefCount() that makes this work the way you expect.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • 1
    Upvoted. This implementation of the `FromConsole` is indeed better than the `Observable.Create`-based implementation, because it correctly handles unsubscription. Something that is not obvious is that the `Observable.Start` by default invokes the supplied action on the `ThreadPool`, which is a desirable behavior. [In other cases](https://stackoverflow.com/questions/66314910/how-to-make-the-rx-callbacks-run-on-the-threadpool) this is not what happens. – Theodor Zoulias Jun 16 '21 at 02:24
  • @TheodorZoulias In what sense does `Observable.Create` not correctly handle unsubscription? – Kevin Krumwiede Jul 06 '21 at 01:02
  • @KevinKrumwiede - The call to `observable.Subscribe(Console.WriteLine)` becomes a blocking call because of the `while (true)` loop. – Enigmativity Jul 06 '21 at 06:15
  • @KevinKrumwiede if you dispose the subscription to the sequence created by `Observable.Create`, the `while (true)` loop will keep spinning, and it will keep consuming the lines entered in the console. To solve this problem, an overload that accepts a `Func` lambda can be used instead. The `CancellationToken` is signaled when the subscription is disposed. – Theodor Zoulias Jul 06 '21 at 06:18
  • @TheodorZoulias - You can't even get to the subscription to dispose it. The call to `Subscribe` is a blocking call thanks to the `while (true)`. – Enigmativity Jul 06 '21 at 08:41
  • @TheodorZoulias Seems to me the problem is `while(true)`, not `Observable.Create`. – Kevin Krumwiede Jul 06 '21 at 17:24
  • @KevinKrumwiede the `while (true)` loop is OK, provided that there is some awaited asynchronous operation inside the loop. The point of my original comment was to compare the `Repeat`-based solution of this answer with the `Observable.Create`-based solution of [my answer](https://stackoverflow.com/a/67990985/11178549). Both solutions are functionally equivalent, with the exception of their behavior when a subscription is disposed. The `Repeat`-based solution is superior in that aspect, but the `Observable.Create`-based solution can be fixed to match the correct behavior. – Theodor Zoulias Jul 06 '21 at 18:15
  • @KevinKrumwiede - No, the problem is also the `Observable.Create`. It's a blocking operator on subscription. It should always be used as a last resort. In situations when you only use `Create` to get transient state then `Observable.Defer` is better. Bottom-line avoid `Create` where possible. – Enigmativity Jul 07 '21 at 00:00
  • @Enigmativity Hmm. The [Intro to Rx](http://introtorx.com/Content/v1.0.10621.0/04_CreatingObservableSequences.html#ObservableCreate) book says, "The `Create` factory method is the preferred way to implement custom observable sequences." Obviously there are circumstances where the most natural way to implement the sequence is by blocking, and that's when the blocking subscription becomes a problem. But to say that `Create` should only be used as a last resort seems backwards. It's fine in almost all circumstances. – Kevin Krumwiede Jul 08 '21 at 20:21
  • @KevinKrumwiede - I disagree. I'm one of three people on this site with a gold badge for `system.reactive`. I feel I've got a reasonable handle on it. – Enigmativity Jul 09 '21 at 00:14
  • @Enigmativity That makes your viewpoint worth considering, but it still requires justification, especially when it directly contradicts other authoritative sources. More specifically, I acknowledge all the facts you've stated; I just don't think it follows from those facts that `Create` should be avoided. You haven't explained what's wrong with `Create` when the thing being observed is already asynchronous, which IME is by far the most common scenario. (to be continued) – Kevin Krumwiede Jul 09 '21 at 00:58
  • Does `Defer` ensure that subscribe returns the `IDisposable` subscription before the asynchronous observable potentially emits elements or terminates? Is that the reason to prefer it? – Kevin Krumwiede Jul 09 '21 at 01:03
  • @KevinKrumwiede - `Defer` allows local state for each subscription, which `Create` also does, but it doesn't subscribe to observer in `Defer` so you can use it cleanly with the standard operators. – Enigmativity Jul 09 '21 at 01:29
  • @KevinKrumwiede - `Create` just encourages coding that introduces deadlocks and race conditions. It's best to avoid for that reason. – Enigmativity Jul 09 '21 at 01:30
  • @Enigmativity Sorry, not buying it. Novice developers do novice developer things, and Rx has an exceptionally steep learning curve. That doesn't make `Create` inherently bad. IMO you're throwing out the baby with the bath water because of a rare gotcha that's not even particularly difficult to understand or overcome in the unlikely event you run into it. – Kevin Krumwiede Jul 09 '21 at 17:19
  • @KevinKrumwiede - It occurs each and every time you use the operator. It's just that the content that you put in the `Create` operator, the subsequent operators, or the subscribe can cause this behaviour to manifest itself as a bug. It's often difficult for experienced Rx coders to diagnose, hence why I say it should be avoided. – Enigmativity Jul 10 '21 at 00:58
0

The problem is that the Console.ReadLine is a blocking method, so the subscription to the FromConsole sequence blocks indefinitely, so the await Task.Delay(1500); line is never reached. You can solve this problem by reading from the console asynchronously, offloading the blocking call to a ThreadPool thread:

static IObservable<string> FromConsole()
{
    return Observable.Create<string>(async observer =>
    {
        while (true)
        {
            observer.OnNext(await Task.Run(() => Console.ReadLine()));
        }
    });
}

You can take a look at this question about why there is no better solution than offloading.

As a side note, subscribing to a sequence without providing an onError handler is not a good idea, unless having the process crash with an unhandled exception is an acceptable behavior for your app. It is especially problematic with sequences produced with Observable.Create<T>(async, because it can lead to weird/buggy behavior like this one: Async Create hanging while publishing observable.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • This is only half the solution. Even with the `await Task.Run(() => Console.ReadLine())` you only get one output yet there are two subscriptions. – Enigmativity Jun 16 '21 at 00:13
  • @Enigmativity in my tests I see both subscriptions working (both `onNext` handlers are invoked), provided that I use the commented `FromConsole().Publish().RefCount();` sequence in the OP's example. – Theodor Zoulias Jun 16 '21 at 00:22
  • I just retested your code and you're only getting one value out yet you have two subscriptions. – Enigmativity Jun 16 '21 at 00:57
  • @Enigmativity [here](https://dotnetfiddle.net/W8v3M6) is my code, and [here](https://snipboard.io/cymDUs.jpg) is a sample output, after typing the lines 'Hello' and 'World'. – Theodor Zoulias Jun 16 '21 at 01:50
  • Your code in the answer doesn't show you are running the `.Publish().RefCount()` observable. You don't show the code and you don't explain it in the answer. I can only assume you're using the OP's code to run the query. – Enigmativity Jun 16 '21 at 02:45
  • You also don't explain why the plain version of the observable fails. – Enigmativity Jun 16 '21 at 02:48
  • My friend the OP is experimenting in their example with various ways to use the `FromConsole` sequence. It happens that currently the `FromConsole().Publish().RefCount();` is commented and the `FromConsole();` is not, but it could be the other way around. I addressed the main issue in the OP's code, the blocking nature of the `Console.ReadLine` method, and I am assuming that the OP will be able to figure out the rest. Btw even with the current state of the OP's example, both subscriptions are effective, with each one observing half of the lines typed in the console. – Theodor Zoulias Jun 16 '21 at 03:09
-1

You need to return a observable without the publish. You can then subscribe to it and do your thing further. Here is an example. When I run it i can readline multiple times.

public class Program
{

    static void Main(string[] args)
    {
        FromConsole().Subscribe(x =>
        {
            Console.WriteLine(x);
        });
    }

    static IObservable<string> FromConsole()
    {
        return Observable.Create<string>(async observer =>
        {
            while (true)
            {
                observer.OnNext(Console.ReadLine());
            }
        });
    }
}
ruben450
  • 140
  • 2
  • 10