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