简体   繁体   中英

Perform action after all Observers complete on Observable sequence

I have some observable sequence, for example:

var period = TimeSpan.FromSeconds(0.5);
var observable = Observable
    .Interval(period)
    .Publish()
    .RefCount();

I want to perform some hard computations for elements of this sequence on a background thread and also perform some final action when all computations are done. So I want something like this:

observable.ObserveOn(Scheduler.Default).Subscribe(i => ComplexComputation1(i));
observable.ObserveOn(Scheduler.Default).Subscribe(i => ComplexComputation2(i));
// next observer must be called only after ComplexComputation1/2 complete on input i
observable.Subscribe(i => FinalAction(i));

Can I do this in Rx? Or maybe this violates some principles of reactive programming and I should use another approach in such situation?

It is very dangerous to have computationally ordered sequences in Reactive patterns.

One thing you can do is have each complex calculation emit an event after it is finished. Then you can have a consuming observer who will perform his calculation once he receives messages that the previous steps are finished.


Another possible solution is to create a concrete sequence block that gets fired regularly. This reduces the parallelizability of the solution.

observable.ObserveOn(Scheduler.Default).Subscribe(i => 
{     
    ComplexComputation1(i));
    ComplexComputation2(i));
    FinalAction(i);
}

To test this out I created the following methods to help illustrate the sequence of events:

public void ComplexComputation1(long i)
{
    Console.WriteLine("Begin ComplexComputation1");
    Thread.Sleep(100);
    Console.WriteLine("End ComplexComputation1");
}

public void ComplexComputation2(long i)
{
    Console.WriteLine("Begin ComplexComputation2");
    Thread.Sleep(100);
    Console.WriteLine("End ComplexComputation2");
}

public void FinalAction(long i)
{
    Console.WriteLine("Begin FinalAction");
    Thread.Sleep(100);
    Console.WriteLine("End FinalAction");
}

Your original code ran like this:

Begin FinalAction
Begin ComplexComputation1
Begin ComplexComputation2
End ComplexComputation2
End FinalAction
End ComplexComputation1
Begin FinalAction
Begin ComplexComputation1
Begin ComplexComputation2
End FinalAction
End ComplexComputation2
End ComplexComputation1
Begin FinalAction
Begin ComplexComputation1
Begin ComplexComputation2
End ComplexComputation2
End ComplexComputation1
End FinalAction
...

It's easy to enforce the code to run in sequence on a single background thread. Just use an EventLoopScheduler .

var els = new EventLoopScheduler();

observable.ObserveOn(els).Subscribe(i => ComplexComputation1(i));
observable.ObserveOn(els).Subscribe(i => ComplexComputation2(i));
// next observer must be called only after ComplexComputation1/2 complete on input i
observable.ObserveOn(els).Subscribe(i => FinalAction(i));

That gives:

Begin ComplexComputation1
End ComplexComputation1
Begin ComplexComputation2
End ComplexComputation2
Begin FinalAction
End FinalAction
Begin ComplexComputation1
End ComplexComputation1
Begin ComplexComputation2
End ComplexComputation2
Begin FinalAction
End FinalAction
Begin ComplexComputation1
End ComplexComputation1
Begin ComplexComputation2
End ComplexComputation2
Begin FinalAction
End FinalAction

But as soon as you introduce Scheduler.Default this doesn't work.

The more-or-less simple option is to do this:

var cc1s = observable.ObserveOn(Scheduler.Default).Select(i => { ComplexComputation1(i); return Unit.Default; });
var cc2s = observable.ObserveOn(Scheduler.Default).Select(i => { ComplexComputation2(i); return Unit.Default; });

observable.Zip(cc1s.Zip(cc2s, (cc1, cc2) => Unit.Default), (i, cc) => i).Subscribe(i => FinalAction(i));

That works as expected.

You get a nice sequence like this:

Begin ComplexComputation1
Begin ComplexComputation2
End ComplexComputation1
End ComplexComputation2
Begin FinalAction
End FinalAction
Begin ComplexComputation2
Begin ComplexComputation1
End ComplexComputation2
End ComplexComputation1
Begin FinalAction
End FinalAction
Begin ComplexComputation1
Begin ComplexComputation2
End ComplexComputation2
End ComplexComputation1
Begin FinalAction
End FinalAction

This seems like a simple case of composition of a nested observable being flattened (SelectMany/Merge/Concat) and Zip

Here I have taken the liberty to assume the Long Running methods return a Task . However if they dont, then the slow blocking synchronous method could instead be wrapped with Observable.Start(()=>ComplexComputation1(x)) .

void Main()
{
    var period = TimeSpan.FromSeconds(0.5);
    var observable = Observable
        .Interval(period)
        .Publish()
        .RefCount();

    var a = observable.Select(i => ComplexComputation1(i).ToObservable())
                .Concat();
    var b = observable.Select(i => ComplexComputation2(i).ToObservable())
                .Concat();

    a.Zip(b, Tuple.Create)
        .Subscribe(pair => FinalAction(pair.Item1, pair.Item2));
}

// Define other methods and classes here
Random rnd = new Random();
private async Task<long> ComplexComputation1(long i)
{
    await Task.Delay(rnd.Next(50, 1000));
    return i;
}
private async Task<long> ComplexComputation2(long i)
{
    await Task.Delay(rnd.Next(50, 1000));
    return i;
}

private void FinalAction(long a, long b)
{

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM