TPL Dataflow BufferBlock线程安全吗?

时间:2018-06-18 14:32:43

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

我有一个相当简单的生产者 - 消费者模式,其中(简化)我有两个生产者,他们生产的产品将由一个消费者消费。

为此我使用System.Threading.Tasks.Dataflow.BufferBlock<T>

创建了一个BufferBlock对象。一个Consumer正在监听此BufferBlock,并处理任何收到的输入。

两个'生产者send data to the BufferBlock`同时

简化为:

BufferBlock<int> bufferBlock = new BufferBlock<int>();

async Task Consume()
{
    while(await bufferBlock.OutputAvailable())
    {
         int dataToProcess = await outputAvailable.ReceiveAsync();
         Process(dataToProcess);
    }
}

async Task Produce1()
{
    IEnumerable<int> numbersToProcess = ...;
    foreach (int numberToProcess in numbersToProcess)
    {
         await bufferBlock.SendAsync(numberToProcess);
         // ignore result for this example
    }
}

async Task Produce2()
{
    IEnumerable<int> numbersToProcess = ...;
    foreach (int numberToProcess in numbersToProcess)
    {
         await bufferBlock.SendAsync(numberToProcess);
         // ignore result for this example
    }
}

我想首先启动消费者,然后将生产者作为单独的任务启动:

var taskConsumer = Consume(); // do not await yet
var taskProduce1 = Task.Run( () => Produce1());
var taskProduce2 = Task.Run( () => Produce2());

// await until both producers are finished:
await Task.WhenAll(new Task[] {taskProduce1, taskProduce2});
bufferBlock.Complete(); // signal that no more data is expected in bufferBlock

// await for the Consumer to finish:
await taskConsumer;

乍一看,这正是生产者 - 消费者的意思:几个生产者在消费者消费生产数据时产生数据。

然而,BufferBlock about thread safety说:

  

不保证所有实例成员都是线程安全的。

我认为TPL中的P意味着平行! 我应该担心吗?我的代码不是线程安全吗? 我应该使用不同的TPL Dataflow类吗?

2 个答案:

答案 0 :(得分:2)

是的,BufferBlock类是线程安全的。我无法通过指向正式文档来备份此声明,因为已从文档中删除了“线程安全”部分。但是我可以在源代码中看到该类包含一个用于同步传入消息的锁对象:

/// <summary>Gets the lock object used to synchronize incoming requests.</summary>
private object IncomingLock { get { return _source; } }

调用Post扩展方法(source code)时,将调用显式实现的ITargetBlock.OfferMessage方法(source code)。下面是此方法的摘录:

DataflowMessageStatus ITargetBlock<T>.OfferMessage(DataflowMessageHeader messageHeader,
    T messageValue, ISourceBlock<T> source, bool consumeToAccept)
{
    //...
    lock (IncomingLock)
    {
        //...
        _source.AddMessage(messageValue);
        //...
    }
}

如果此类或TPL Dataflow库中包含的任何其他XxxBlock类都不是线程安全的,确实会很奇怪。这将严重妨碍这个强大库的易用性。

答案 1 :(得分:0)

我认为ActionBlock<T>更适合你的工作,因为它有一个内置缓冲区,许多生产者可以通过它来发送数据。默认块选项处理单个后台任务上的数据,但您可以为并行度和有界容量设置新值。使用ActionBlock<T>,确保线程安全的主要关注区域将在您传递的委托中处理每条消息。该函数的操作必须独立于每条消息,即不像任何Parrallel...函数那样修改共享状态。

public class ProducerConsumer
{
    private ActionBlock<int> Consumer { get; }

    public ProducerConsumer()
    {
        Consumer = new ActionBlock<int>(x => Process(x));            
    }

    public async Task Start()
    {
        var producer1Tasks = Producer1();
        var producer2Tasks = Producer2();
        await Task.WhenAll(producer1Tasks.Concat(producer2Tasks));
        Consumer.Complete();
        await Consumer.Completion;
    }

    private void Process(int data)
    {
        // process
    }

    private IEnumerable<Task> Producer1() => Enumerable.Range(0, 100).Select(x => Consumer.SendAsync(x));

    private IEnumerable<Task> Producer2() => Enumerable.Range(0, 100).Select(x => Consumer.SendAsync(x));
}