使用TPL Dataflow封装以动作块结尾的管道

时间:2015-09-08 14:02:09

标签: c# task-parallel-library tpl-dataflow

TPL Dataflow提供了非常有用的功能:

IPropagatorBlock<TInput, TOutput>

使您能够将多个块封装到单个转换块中。它返回一个

public static ITargetBlock<TStart> Encapsulate<TStart, TEnd>(
        ITargetBlock<TStart> startBlock, 
        ActionBlock<TEnd> endBlock)

表示管道的起始和结束块。

但是,如果我的管道中的最后一个块是ActionBlock,我不能使用它,因为ActionBlock不是SourceBlock,并且该函数的返回类型将是ITargetBlock,而不是IPropagatorBlock。

基本上,我正在寻找的是这样的功能:

internal sealed class EncapsulatingTarget<TStart, TEnd> : ITargetBlock<TStart>
{
        private readonly ITargetBlock<TStart> startBlock;

        private readonly ActionBlock<TEnd> endBlock;

        public EncapsulatingTarget(ITargetBlock<TStart> startBlock, ActionBlock<TEnd> endBlock)
        {
            this.startBlock = startBlock;
            this.endBlock = endBlock;
        }

        public Task Completion
        {
            get { return this.endBlock.Completion; }
        }

        public void Complete()
        {
            this.startBlock.Complete();
        }

        void IDataflowBlock.Fault(Exception exception)
        {
            if (exception == null)
            {
                throw new ArgumentNullException("exception");
            }

            this.startBlock.Fault(exception);
        }

        public DataflowMessageStatus OfferMessage(
            DataflowMessageHeader messageHeader, 
            TStart messageValue, 
            ISourceBlock<TStart> source, 
            bool consumeToAccept)
        {
            return this.startBlock.OfferMessage(messageHeader, messageValue, source, consumeToAccept);
        }
    }

这是一个明智的事情,或者我错过了一些简单的东西?我不太确定如何 写它 - 特别是完成连接。我需要创建自己的自定义块类型吗?

修改

好的,所以看过@Panagiotis Kanavos的回复,做了一些修补,我想出了这个。这基于EncapsulatingPropagator类,它是现有DataflowBlock.Encapsulate方法使用的类:

top 10

2 个答案:

答案 0 :(得分:4)

Encapsulate不用于抽象现有管道,它用于创建传播器块,该块需要使用现有块和链接无法实现的自定义行为。

例如,Sliding Window示例缓冲发布到其输入块的所有传入消息,并在滑动窗口到达其输出块时输出一批所有检索到的消息。

该方法的名称造成了很多混乱,但是当你理解它们的目的时它们才有意义:

  • target 参数是前面块将连接到的目标(输入)端点以发送消息。在这种情况下,处理传入消息并决定是否发布到输出(源)块的ActionBlock是有意义的。
  • source 参数是成功步骤将连接到的接收消息的源(输出)端点。将ActionBlock用作源是没有意义的,因为它没有有任何输出。

接受ActionBlock方法为Encapsulate的{​​{1}}变体没有用,因为您可以简单地从任何上一步骤链接到操作块。

修改

如果你想 modularize 一个管道,即将它分解为可重用,更易于管理,你可以创建一个构造的类,你可以使用一个普通的旧类。在该类中,您正常构建管道片段,链接块(确保传播完成),然后将最后一步的第一步和完成任务公开为公共属性,例如:

source

要将片段连接到管道,您只需要从管道块链接到公开的class MyFragment { public TransformationBlock<SomeMessage,SomeOther> Input {get;} public Task Completion {get;} ActionBlock<SomeOther> _finalBlock; public MyFragment() { Input=new TransformationBlock<SomeMessage,SomeOther>(MyFunction); _finalBlock=new ActionBlock<SomeOther>(MyMethod); var linkOptions = new DataflowLinkOptions {PropagateCompletion = true} Input.LinkTo(_finalBlock,linkOptions); } private SomeOther MyFunction(SomeMessage msg) { ... } private void MyMethod(SomeOther msg) { ... } } 块。等待完成,只需等待公开的Input任务。

如果需要,您可以在这里停止,或者您可以实现ITargetBlock以使片段看起来像Target块。您只需将所有方法委托给Input块,将Completion属性委托给最终块。

例如:

Completion

编辑2

使用@bornfromanegg的类可以将构建片段的行为与暴露输入和完成的样板分开:

class MyFragment:ITargetBlock<SomeMessage> 
{
    ....

    public Task Completion {get;}

    public void Complete()
    {
        Input.Complete()
    };

    public void Fault(Exception exc)
    {
        Input.Fault(exc);
    }

    DataflowMessageStatus OfferMessage(DataflowMessageHeader messageHeader,
        TInput messageValue,ISourceBlock<TInput> source,bool consumeToAccept)
    {
        return Input.OfferMessage(messageHeader,messageValue,source,consumeToAccept);
    }
}

答案 1 :(得分:2)

在我的情况下,我想封装一个包含多个最终ActionBlock的网络,并总结完成,因此编辑问题中概述的解决方案不起作用。

因为与“最终块”的唯一交互围绕完成,所以仅提供封装的完成任务就足够了。 (根据建议添加了目标 - 动作构造函数。)

public class EncapsulatingTarget<TInput> : ITargetBlock<TInput>
{
    private readonly ITargetBlock<TInput> startBlock;

    private readonly Task completion;

    public EncapsulatingTarget(ITargetBlock<TInput> startBlock, Task completion)
    {
        this.startBlock = startBlock;
        this.completion = completion;
    }

    public EncapsulatingTarget(ITargetBlock<TStart> startBlock, ActionBlock<TEnd> endBlock)
    {
        this.startBlock = startBlock;
        completion = endBlock.Completion;
    }

    public Task Completion => completion;

    public void Complete()
    {
        startBlock.Complete();
    }

    void IDataflowBlock.Fault(Exception exception)
    {
        if (exception == null)
        {
            throw new ArgumentNullException("exception");
        }

        startBlock.Fault(exception);
    }

    public DataflowMessageStatus OfferMessage(
        DataflowMessageHeader messageHeader,
        TInput messageValue,
        ISourceBlock<TInput> source,
        bool consumeToAccept)
    {
        return startBlock.OfferMessage(messageHeader, messageValue, source, consumeToAccept);
    }
}

示例用法:

public ITargetBlock<Client.InputRecord> BuildDefaultFinalActions()
{
    var splitter = new BroadcastBlock<Client.InputRecord>(null);
    var getresults = new TransformManyBlock(...);    // propagator
    var saveinput = new ActionBlock(...);
    var saveresults = new ActionBlock(...);

    splitter.LinkTo(saveinput, PropagateCompletion);
    splitter.LinkTo(getresults, PropagateCompletion);
    getresults.LinkTo(saveresults, PropagateCompletion);

    return new Util.EncapsulatedTarget<Client.InputRecord>(splitter, Task.WhenAll(saveinput.Completion, saveresults.Completion));
}

我本可以签名EncapsulatingTarget<T>(ITargetBlock<T> target, params Task[] completions)并将WhenAll(...)移到构造函数中,但不想对所需的完成通知做出假设。