问题
Consider the simple program below. It has an observable of integers and a function to calculate whether the most recently published integer is even or odd. Unexpectedly, the program reports whether the most recent number is even/odd BEFORE it reports that the number changed.
static void Main(string[] args) {
int version = 0;
var numbers = new Subject<int>();
IObservable<bool> isNumberEven = numbers.Select(i => i % 2 == 0);
isNumberEven
.Select(i => new { IsEven = i, Version = Interlocked.Increment(ref version) })
.Subscribe(i => Console.WriteLine($"Time {i.Version} : {i.IsEven}"));
numbers
.Select(i => new { Number = i, Version = Interlocked.Increment(ref version) })
.Subscribe(i => Console.WriteLine($"Time {i.Version} : {i.Number}"));
numbers.OnNext(1);
numbers.OnNext(2);
numbers.OnNext(3);
Console.ReadLine();
}
The output is:
Time 1 : False
Time 2 : 1
Time 3 : True
Time 4 : 2
Time 5 : False
Time 6 : 3
I thought that changing the number would set off a cascade of downstream effects and these would be reported IN THE ORDER THEY HAPPEN. Swapping the subscription order will swap the way results are reported. I understand that rx is asynchronous and it is possible for things to happen in non-deterministic order. If I used .Delay() or web calls in my functions I can't be sure when the results will be reported. But in this situation, I'm very surprised.
Why is this a big deal? I think this means that if I want to try to correlate function inputs and outputs (like printing numbers as they are published along with whether they are even or odd), I MUST include the input parameters in the output results, like this:
var isNumberEven = numbers.Select(i => new {
Number = i,
IsEven = i % 2 == 0
});
I thought I could build a bunch of small simple functions and then compose them using the rx operators to accomplish sophisticated calculations. But maybe I can't use the rx operators to combine/join/correlate results. I have to correlate the inputs and outputs myself when I define each function.
In some cases I can use the rx operators to correlate results. If every input generated an output, I could zip the two. But as soon as you do something like Throttle the input it doesn't work anymore.
This version of the program does seem to report whether numbers are even or odd in a reasonable way.
static void Main(string[] args) {
var numbers = new Subject<int>();
var isNumberEven = numbers.Select(i => i % 2 == 0);
var publishedNumbers = numbers.Publish().RefCount();
var report =
publishedNumbers
.GroupJoin(
isNumberEven,
(_) => publishedNumbers,
(_) => Observable.Empty<bool>(),
(n, e) => new { Number = n, IsEven = e })
.SelectMany(i => i.IsEven.Select(j => new { Number = i.Number, IsEven = j }));
report.Subscribe(i => Console.WriteLine($"{i.Number} {(i.IsEven ? "even" : "odd")}"));
numbers.OnNext(1);
numbers.OnNext(2);
numbers.OnNext(3);
Console.ReadLine();
}
The output looks like:
1 odd
2 even
3 odd
But I don't know if this was a lucky coincidence or whether I can rely on it. What operations in Rx happen in a deterministic order? Which ones are unpredictable? Should I be defining all my functions to include the input parameters in the results?
回答1:
Your first program is behaving exactly as I would expect it to, and deterministically so.
I understand that rx is asynchronous and it is possible for things to happen in non-deterministic order.
Things only happen in non-deterministic order if you introduce non-deterministic behaviour (like concurrency/Scheduling), otherwise Rx is deterministic.
There are several issues/misconceptions at play here.
1) Mutable external state - version
2) Use of subject (but not really an issue at all in this sample)
3) Misunderstanding of how the callbacks are issued.
Lets just focus on 3). If we take you code and unwrap it to its basic callsites, you may see how simple Rx is under the covers.
numbers.OnNext(1);
the subject will look up it's subscriptions and OnNext
each of them in the order that they subscribed.
IObservable<bool> isNumberEven = numbers.Select(i => i % 2 == 0);
isNumberEven
.Select(i => new { IsEven = i, Version = Interlocked.Increment(ref version) })
.Subscribe(i => Console.WriteLine($"Time {i.Version} : {i.IsEven}"));
can also be reduced to
numbers.Select(i => i % 2 == 0)
.Select(i => new { IsEven = i, Version = Interlocked.Increment(ref version) })
.Subscribe(i => Console.WriteLine($"Time {i.Version} : {i.IsEven}"));
One could argue, that as isNumberEven
is never used anywhere else, that you should reduce it to this.
So we can see we have our first subscriber. And effectively the code it would run is this
private void HandleOnNext(int i)
{
var isEven = i % 2 == 0
var temp = new { IsEven = isEven , Version = Interlocked.Increment(ref version) };
Console.WriteLine($"Time {temp .Version} : {temp .IsEven}");
}
Our second subscriber (because the .Subscribe(
method was called after the even number subscription), is the numbers
subscriber.
His code can effectively be boiled down to
private void HandleOnNext(int i)
{
var temp = new { Number = i, Version = Interlocked.Increment(ref version) };
Console.WriteLine($"Time {temp.Version} : {temp.Number}");
}
So once you have fully deconstructed the code, you end up with basically this
void Main()
{
int version = 0;
//numbers.OnNext(1);
ProcessEven(1, ref version);
ProcessNumber(1, ref version);
//numbers.OnNext(2);
ProcessEven(2, ref version);
ProcessNumber(2, ref version);
//numbers.OnNext(3);
ProcessEven(3, ref version);
ProcessNumber(3, ref version);
}
// Define other methods and classes here
private void ProcessEven(int i, ref int version)
{
var isEven = i % 2 == 0;
var temp = new { IsEven = isEven, Version = Interlocked.Increment(ref version) };
Console.WriteLine($"Time {temp.Version} : {temp.IsEven}");
}
private void ProcessNumber(int i, ref int version)
{
var temp = new { Number = i, Version = Interlocked.Increment(ref version) };
Console.WriteLine($"Time {temp.Version} : {temp.Number}");
}
Once all the callbacks and subscriptions are reified, then you can see that this is no magic happening and everything is deterministic.
Should I be defining all my functions to include the input parameters in the results?
To answer your question (which I hesitate to do so given your misunderstanding of Rx), you only need to do so when the order of the result sequence will be non deterministic.
An example of this might be if you issued multiple web-requests at once.
You can not be sure that they will all respond in the order you sent them.
However you can force these scenarios back into line with the usage of operators like Concat
来源:https://stackoverflow.com/questions/38364746/how-to-correlate-function-inputs-and-outputs