C# .Net RX:跟踪并行执行的进度

C# .Net RX:跟踪并行执行的进度,c#,system.reactive,C#,System.reactive,我需要并行执行多个长时间运行的操作,并希望以某种方式报告进度。从我最初的研究来看,IObservable似乎适合这个模型。我的想法是调用一个返回IObservable of int的方法,其中int被报告为完成百分比,并行执行在退出一个方法后立即开始,该可观察对象必须是热可观察对象,以便所有订阅者在特定时间点学习相同的进度信息,例如,迟到的订户可能只知道整个执行已经完成,没有更多的进度需要跟踪 我发现解决这个问题的最接近的方法是使用Observable.ForkJoin和Observable.S

我需要并行执行多个长时间运行的操作,并希望以某种方式报告进度。从我最初的研究来看,IObservable似乎适合这个模型。我的想法是调用一个返回IObservable of int的方法,其中int被报告为完成百分比,并行执行在退出一个方法后立即开始,该可观察对象必须是热可观察对象,以便所有订阅者在特定时间点学习相同的进度信息,例如,迟到的订户可能只知道整个执行已经完成,没有更多的进度需要跟踪

我发现解决这个问题的最接近的方法是使用Observable.ForkJoin和Observable.Start,但我无法理解如何使它们成为我可以从方法返回的单个Observable

请分享您对如何实现这一目标的想法,或者使用.Net RX解决此问题的另一种方法。

这里有一种方法

// setup a method to do some work, 
// and report it's own partial progress
Func<string, IObservable<int>> doPartialWork = 
    (arg) => Observable.Create<int>(obsvr => {
        return Scheduler.TaskPool.Schedule(arg,(sched,state) => {
            var progress = 0;
            var cancel = new BooleanDisposable();
            while(progress < 10 && !cancel.IsDisposed)
            {
                // do work with arg
                Thread.Sleep(550);
                obsvr.OnNext(1); //report progress
                progress++;
            }
            obsvr.OnCompleted();
            return cancel;
        });
    });

var myArgs = new[]{"Arg1", "Arg2", "Arg3"};

// run all the partial bits of work
// use SelectMany to get a flat stream of 
// partial progress notifications
var xsOfPartialProgress =  
        myArgs.ToObservable(Scheduler.NewThread)
              .SelectMany(arg => doPartialWork(arg))
                  .Replay().RefCount();

// use Scan to get a running aggreggation of progress
var xsProgress = xsOfPartialProgress
                   .Scan(0d, (prog,nextPartial)  
                            => prog + (nextPartial/(myArgs.Length*10d)));
//设置一个方法来做一些工作,
//并报告它自己的部分进展
Func doPartialWork=
(arg)=>Observable.Create(obsvr=>{
返回Scheduler.TaskPool.Schedule(arg,(sched,state)=>{
var进程=0;
var cancel=新布尔一次性();
while(进度<10&!取消.IsDisposed)
{
//与arg一起工作
睡眠(550);
obsvr.OnNext(1);//报告进度
进步++;
}
obsvr.OnCompleted();
返回取消;
});
});
var myArgs=new[]{“Arg1”、“Arg2”、“Arg3”};
//运行所有部分工作
//使用SelectMany可获得一个平坦的数据流
//部分进度通知
var xsOfPartialProgress=
myArgs.ToObservable(Scheduler.NewThread)
.SelectMany(arg=>doPartialWork(arg))
.Replay().RefCount();
//使用扫描获取正在运行的进度汇总
var xsProgress=xsOfPartialProgress
.扫描(0d,(程序,下一部分)
=>prog+(下一部分/(myArgs.Length*10d));

为了使热可观察性成为可能,我可能会从一个方法开始,该方法使用
行为子对象作为返回值和操作报告进度的方式。如果您只想看示例,请跳到结尾。这个答案的其余部分解释了这些步骤

为了回答这个问题,我假设您的长时间运行的操作没有自己的异步调用方式。如果他们这样做了,下一步可能会有所不同。接下来要做的事情是使用
IScheduler
将工作发送到另一个线程。如果需要,您可以允许调用方选择工作发生的位置,方法是创建一个重载,将调度程序作为参数(在这种情况下,不选择默认调度程序的重载)。
IScheduler.Scheduler
有很多重载,其中有几个是扩展方法,所以您应该仔细查看它们,看看哪个最适合您的情况;我使用的是只需执行
操作的on。如果有多个操作可以并行运行,则可以多次调用
scheduler.Schedule

这其中最困难的部分可能是确定在任何给定点的进展情况。如果同时进行多个操作,则可能需要跟踪完成了多少操作,以了解当前的进度。根据你提供的信息,我再具体不过了

最后,如果您的操作是可取消的,您可能希望将
CancellationToken
作为参数。当操作在调度程序队列中时,您可以使用此选项取消该操作,然后再开始该操作。如果您正确编写操作代码,它也可以使用令牌进行取消

IObservable<int> DoStuff(/*args*/, 
                         CancellationToken cancel,
                         IScheduler scheduler)
{
    BehaviorSubject<int> progress;
    //if you don't take it as a parameter, pick a scheduler
    //IScheduler scheduler = Scheduler.ThreadPool;

    var disp = scheduler.Schedule(() =>
    {
        //do stuff that needs to run on another thread

        //report progres
        porgress.OnNext(25);
    });
    var disp2 = scheduler.Schedule(...);

    //if the operation is cancelled before the scheduler has started it,
    //you need to dispose the return from the Schedule calls
    var allOps = new CompositeDisposable(disp, disp2);
    cancel.Register(allOps.Dispose);

    return progress;
}
IObservable DoStuff(/*args*/,,
取消令牌取消,
IScheduler(调度程序)
{
行为主体进展;
//如果不将其作为参数,请选择一个调度程序
//isScheduler scheduler=scheduler.ThreadPool;
var disp=scheduler.Schedule(()=>
{
//做一些需要在另一个线程上运行的事情
//报告进展
porgress.OnNext(25);
});
var disp2=scheduler.Schedule(…);
//如果该操作在计划程序启动之前取消,
//您需要处理来自调度调用的返回
var allOps=新的复合可编程(disp,disp2);
取消注册(allOps.Dispose);
返回进度;
}

您的函数会创建一个冷可观察的对象。这项工作并不是从最初调用
doPartialWork
开始的,而是从每次订阅返回的可观察对象开始的。这是一个非常好的示例和解释。非常感谢。我还有一个问题。如果我的长期运行的ops被分成几个组,我需要连续执行组,同时并行执行组内的ops,该怎么办?在这种情况下,我是否应该使用任务库,它允许通过ContinueWith而不是scheduler进行此类链接,但保留使用BehaviorSubject?@andriys这是一个选项。另一种选择是将每个组分成一个单独的函数,返回一个IObservable。这些功能可以通过
Observable.Concat
链接在一起。但是,您将希望在冷链中使除第一个之外的所有对象都可见<代码>可观察。延迟
允许您将热可观察转换为冷可观察。如果操作不受计算限制,而是受网络限制,例如并行请求不同的web服务,您将如何更改示例?@andriys我认为不会有太大变化。您可能会使用不同的调度程序,但我不想麻烦您。