当MaxDegreeOfParallelism> 1时,等效于Parallel.For的localInit和localFinally在TPL数据流块中使用

时间:2019-02-01 12:17:13

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

我有一个TransformBlock<int, int>,其中有MaxDegreeOfParallelism = 6。我也确定了传递到块的构造Func<int, int>(为每个发布的条目被执行)可以被逻辑地划分成一个昂贵的初始化程序和身体发生变异函数的局部变量。这将是更有效的,如果我可以重构功能到一个名为类TransformBlockState,执行初始化一次,每个并发操作(就像Parallel.For的{​​{1}}回调),然后让TPL数据流确保状态一次不会被一个以上的项目突变。

重构之前:

localInit

重构后:

Func<int, int> original = x => {
    // method local variables
    // expensive initialization routine to setup locals
    // perform action on local variables
    // potentially expensive teardown
}

做了类似于public sealed class TransformBlockState<TIn, TOut> : IDisposable { // instance state public TransformBlockState() { // expensive initialization routine } public TOut Transform(TIn value) { // called many times but never concurrently for the same instance } public void Dispose() { // tear down state } }(关于localInit)和.ctor(关于localFinally)回调已经在TPL数据流库中存在?

我想避免具有Dispose(大量不必要锁定的),我想避免存储ConcurrentStack<TransformBlockState>TransformBlockState字段(因为没有保证的{{1} }不会在多个线程上(顺序地,显然地)或在单个线程上的多个[ThreadStatic]上运行(也许都在I / O上阻塞)。

3 个答案:

答案 0 :(得分:0)

如果要使用有状态块TransformBlock(或ActionBlock),则可以创建一个函数,该函数创建该块并将状态放入局部变量中并捕获它们:

private IPropagatorBlock<int,int> CreateMyBlock()
{
    var state = 0;
    return new TransformBlock<int,int>( x => x+state++ );
}

这样,您的类是由编译器隐式创建的。

答案 1 :(得分:0)

没有等效于loclaInitlocalFinally。您可以使用块的流水线创建类似的行为,或者如果那是昂贵的初始化,则可以使用连接池。但是您可能需要重新考虑问题,TPL-Dataflow可能不是最合适的选择。如果不了解确切的问题就很难解决。但通常,任何一次初始化/每次输入都应在流外部完成并传递。

但是,就像我说的那样,您可以使用管道来获取类似Parallel.Foreach的内容,尽管它可能并不是您真正想要的。

public class DataflowPipeline
{
    private TransformBlock<IEnumerable<int>, IEnumerable<Locals>> Initialize { get; }
    private TransformManyBlock<IEnumerable<Locals>, Locals> Distribute { get; }
    private TransformBlock<Locals, Result> Compute { get; }
    //other blocks, results, disposal etc.


    public DataflowPipeline()
    {
        var sequential = new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = 1 };
        var parallel = new ExecutionDataflowBlockOptions() { MaxDegreeOfParallelism = 6 };

        Initialize = new TransformBlock<IEnumerable<int>, IEnumerable<Locals>>(
            inputs => inputs.Select(x => new Locals() { ExpensiveItem = string.Empty, Input = x }),
            sequential);
        Distribute = new TransformManyBlock<IEnumerable<Locals>, Locals>(x => x, sequential);
        Compute = new TransformBlock<Locals, Result>(
            local => new Result() { ExpensiveItem = local.ExpensiveItem, Output = local.Input * 2 },
            parallel);

        //Other blocks, link, complete etc.
    }
}

答案 2 :(得分:0)

我认为我有一个更好的例子-我需要从航空公司获得几千张机票记录(实际上是GDS)。为此,我需要先建立一个昂贵的 session ,然后才能发送SOAP或REST请求。会议受到限制,所以我真的不想为每张票创建一个新的会议。这样会使每个请求 所需时间加倍,并且浪费金钱和资源。

创建自定义块似乎是解决方案,但实际上不是那样好。 数据流建立处理消息流的处理块的管道。试图使它们以不同的方式工作将与它们对数据流模型的基本假设产生冲突。

例如,任务用于并行性,节流和负载平衡-MaxMessagesPerTask选项在接收到最大数量的消息后终止任务,因此一个任务不会长时间占用CPU。为每个任务创建和销毁会话会破坏该机制,并且 最终会创建不必要的会话。

池化

处理此问题的一种方法是使用一个对象池,该对象池由将由块使用的“昂贵”对象(在本例中为Session)使用。令人发指的是,Microsoft.Extensions.ObjectPool软件包提供了这样一个池。文档are non-existent被欺骗性地放置在ASP.NET树中,但这是一个独立的.NET Standard 2.0软件包。 Github source在外观上很简单,该类使用Interlocked.CompareExchange来避免锁定。甚至还有一个LeakTrackingObjectPool实现。

如果我过去知道这一点,我可能会写:

var pool = new DefaultObjectPool<Session>(new DefaultPooledObjectPolicy<Session>());

DefaultPooledObjectPolicy策略仅使用new创建一个新实例。创建新策略很容易,例如使用自己的创建逻辑甚至是工厂方法的策略:

public class SessionPolicy : DefaultPooledObjectPolicy<Session>
{
    public override Session Create()
    {
        //Do whatever is needed here
        return session;
    }
}

重定向

另一种选择是使用多个块实例,并使源块链接到所有这些实例。为了避免将所有消息发送到第一个块,需要有一定的容量。假设我们有这个工厂方法:

TransformBlock<TIn,TOut> CreateThatBlockWithSession<TIn,TOut>(Settings someSettings)
{
    var session=CreateSomeSessionFrom(someSettings);
    var bounded=new DataflowBlockOptions {BoundedCapacity =1};
    return new TransformBlock<TIn,TOut>(msg=>FunctionThatUses(msg,session),bounded);
}

并使用它来创建多个块:

_blocks=Enumerable.Range(0,10)
                  .Select(_=>CreateThatBlockWithSession(settings))
                  .ToArray();

源块可以连接到所有这些块:

foreach(var target in _blocks)
{
    _source.LinkTo(target,options);
}

然后,将所有这些块链接到下一个块。这里的棘手部分是我们不能仅仅传播完成。如果其中一个块已完成,即使其他块中有消息正在等待,它也会强制下一个块完成。

解决方案是使用Task.WhenAllContinueWith将完成工作推进到下一个块:

foreach(var target in _blocks)
{
    target.LinkTo(_nextBlock);
}

var allTasks=_blocks.Select(blk=>blk.Completion);
Task.WhenAll(allTasks)
    .ContinueWith(_=>_nextBlock.Complete());

更强大的实现将检查所有任务的IsFaulted状态,如果其中一个失败,则在下一个块调用Fault()