C# 反应式管道-如何控制并行性?

C# 反应式管道-如何控制并行性?,c#,.net,parallel-processing,system.reactive,C#,.net,Parallel Processing,System.reactive,我正在构建一个简单的处理管道,其中一个项目作为输入获取,它由多个处理器以顺序方式操作,最后输出。下图描述了总体架构: 当前的工作方式:管道正在尽可能快地从提供者获取项目。一旦取回一个项目,它就被传递给处理器。处理项目后,将通知输出。虽然以顺序方式处理单个项目,但根据从提供程序获取多个项目的速度,可以并行处理多个项目 从管道创建并返回的IObservable如下所示: return Observable.Create<T>(async observer => { whil

我正在构建一个简单的处理管道,其中一个项目作为输入获取,它由多个处理器以顺序方式操作,最后输出。下图描述了总体架构:

当前的工作方式:管道正在尽可能快地从提供者获取项目。一旦取回一个项目,它就被传递给处理器。处理项目后,将通知输出。虽然以顺序方式处理单个项目,但根据从提供程序获取多个项目的速度,可以并行处理多个项目

从管道创建并返回的IObservable如下所示:

return Observable.Create<T>(async observer =>
{
    while (_provider.HasNext)
    {
        T item = await _provider.GetNextAsync();
        observer.OnNext(item);
    }                
}).SelectMany(item => Observable.FromAsync(() =>
    _processors.Aggregate(
        seed: Task.FromResult(item),
        func: (current, processor) => current.ContinueWith( // Append continuations.
            previous => processor.ProcessAsync(previous.Result))
            .Unwrap()))); // We need to unwrap Task{T} from Task{Task{T}};
缺少的部分:我需要一个控制机制来控制在任何给定时间max可以在管道中包含多少项

例如,如果“最大并行处理数”为3,则会产生以下工作流:

获取项目1并将其传递给处理器。 项目2被提取并传递给处理器。 获取项目3并将其传递给处理器。 项目1已完成处理。 获取项目4并将其传递给处理器。 项目3已完成处理。 获取项目5并将其传递给处理器。 等
您可能需要重新排列发布的代码,但这是一种方法:

var eventLoopScheduler = new EventLoopScheduler ();
(from semaphore in Observable.Return(new Semaphore(2,2))
 from input in GetInputObs()
 from getAccess in Observable.Start(() => semaphore.WaitOne(),eventLoopScheduler)
 from output in ProcessInputOnPipeline(input)
        .SubscribeOn(Scheduler.Default) 
        .Finally(() => semaphore.Release())
 select output)
 .Subscribe(x => Console.WriteLine(x), ex => {});
我已经将你的管道建模为1个可观察对象,实际上它是由几个链接在一起的较小的可观察对象组成的

关键是要确保不管管道如何终止Empty/Error,信号量都会被释放,否则流可能会挂起,因此在信号量上使用Finally调用Release。可能值得考虑在可观察的管道上添加一个超时,如果它可能永远不会完成/OnError

编辑:

根据下面的评论,我添加了一些关于信号量访问的调度,这样我们就不会阻止任何人将这些输入推送到我们的流中。我使用了EventLoopScheduler,以便所有信号量访问请求都将排队并在一个线程上执行

编辑:不过我更喜欢Paul的答案——简单、更少的调度、更少的同步。merge在内部使用队列。

merge提供了一个重载,需要花费大量时间

它的签名看起来像:IObservable合并这个IObservable源,int-maxConcurrency

以下是您的示例的外观。我还重构了一些其他代码,您可以选择使用或不使用这些代码:

return Observable
//Reactive while loop also takes care of the onComplete for you
.While(() => _provider.HasNext, 
       Observable.FromAsync(_provider.GetNextAsync))
//Makes return items that will only execute after subscription
.Select(item => Observable.Defer(() => {
  return _processers.Aggregate(
    seed: Observable.Return(item),
    func: (current, processor) => current.SelectMany(processor.ProcessAsync)); 
  }))
 //Only allow 3 streams to be execute in parallel.
.Merge(3);
要分解它的功能

While将检查每个迭代,如果_provider.HasNext为true, 如果是这样,它将重新订阅以获取下一个值 _提供程序,否则它将发出onCompleted 在select内部,创建了一个新的可观察流,但尚未使用DEDER进行评估 返回的IObservable被传递到Merge,Merge同时订阅最多3个可观测值。 内部可观察对象在订阅时进行最终评估。 备选案文1

如果您还需要控制并行请求的数量,那么您需要变得更加棘手,因为您需要发出信号,表明您的可观察对象已准备好接受新值:

return Observable.Create<T>(observer => 
{
  var subject = new Subject<Unit>();
  var disposable = new CompositeDisposable(subject);

  disposable.Add(subject
    //This will complete when provider has run out of values
    .TakeWhile(_ => _provider.HasNext)
    .SelectMany(
      _ => _provider.GetNextAsync(),
     (_, item) => 
     {
       return _processors
        .Aggregate(
         seed: Observable.Return(item),
         func: (current, processor) => current.SelectMany(processor.ProcessAsync))
        //Could also use `Finally` here, this signals the chain
        //to start on the next item.
        .Do(dontCare => {}, () => subject.OnNext(Unit.Default));
     }
    )
    .Merge(3)
    .Subscribe(observer));

  //Queue up 3 requests for the initial kickoff
  disposable.Add(Observable.Repeat(Unit.Default, 3).Subscribe(subject.OnNext));

  return disposable;
});

使用信号灯效果很好!两个问题:1.SubscribeOnScheduler.Default的用途是什么?2我想知道是否有什么解决方法可以阻止线程,等待信号量发出信号?我可能想得太多了:在Observable.Returnnew信号量2,2中使用from信号量是一个非常聪明的举动。隔离observable中的状态做得很好。@discoultan-1子脚本的目的是在线程池上调度管道,这意味着您的流可以继续处理其他输入。2公平点。将通过编辑修改解决方案。感谢您的贡献!虽然您的解决方案非常有效,但我还是求助于@paulpdaniels的解决方案,因为他成功地避免了手动使用线程构造(如信号量)。感谢您的贡献!虽然合并控制将并行处理多少项,但仍会尽快提供这些项,这也需要加以限制。似乎使用Merge不会影响内部的代码运行。While=>\u provider.HasNext,Observable.fromsync\u provider.GetNextAsync。@discoultan啊,我假设GetNextAsync是某种长时间运行的方法,自然会限制速度,但您是正确的,尽管它不与合并相关联。它仍然一次只允许一个GetNextAsync运行,因此速度将与该方法最终返回的速度挂钩。它应该基于某个统一的时间常数进行查询吗?你是正确的,速度与方法最终返回的速度有关,这很好。我需要限制的是,在任何给定的时间内,允许返回并在管道中的数量。例如,如果限制为3,这意味着可以按顺序提取3个项目,只有在其中一个项目完成处理/输出后才能提取第四个项目
放置管道。@discoultan我用一个示例更新了我的答案,该示例将处理该需求。基本上,你需要使用一个主题来通知链何时可以开始处理下一个值。啊,是的,这是正确的,我应该这样做。Subscribesubject.OnNext将阻止整个方法启动。不管怎样,我很高兴它起作用了!