TPL数据流与普通信号灯

时间:2018-07-31 14:41:11

标签: task task-parallel-library semaphore dataflow tpl-dataflow

我需要进行可扩展的流程。该进程主要具有I / O操作,并且具有一些次要的CPU操作(主要是反序列化字符串)。该过程在数据库中查询URL列表,然后从这些URL中获取数据,将下载的数据反序列化为对象,然后将其中的某些数据持久化为crm动态以及另一个数据库。之后,我需要更新第一个处理了URL的数据库。部分要求是使并行度可配置。

最初,我想通过一系列等待任务来实现它,并使用信号量来限制并行度-非常简单。然后,我在这里阅读了@Stephen Cleary的一些文章和答案,这些文章和答案建议使用TPL Dataflow,我认为这可能是一个不错的选择。但是,我想确保通过使用Dataflow来使代码“复杂化”,这是值得的。我还建议使用一个ForEachAsync extension method,它也很容易使用,但是由于它对集合进行分区的方式,我不确定它是否不会引起内存开销。

在这种情况下,TPL Dataflow是一个不错的选择吗?它比Semaphore或ForEachAsync方法更好吗-如果通过TPL DataFlow实现它比其他每个选项(Semaphore / ForEachASync),我实际上将获得什么好处?

2 个答案:

答案 0 :(得分:6)

  

该进程主要具有IO操作以及一些次要的CPU操作(主要是反序列化字符串)。

几乎就是I / O。除非这些字符串巨大,否则反序列化将不值得并行化。您正在做的CPU工作类型会因为噪音而消失。

因此,您将专注于并发异步。

    如您所见,
  • SemaphoreSlim是实现此目的的标准模式。
  • TPL Dataflow还可以进行并发(异步形式和并行形式)。

ForEachAsync可以采用多种形式;请注意,在您引用的blog post中,此方法有 5 个不同的实现,每个实现都是有效的。 “这里有许多可能用于迭代的语义,每种语义都会导致不同的设计选择和实现。”出于您的目的(不希望CPU并行化),您不应该考虑使用Task.Run或分区。在异步并发世界中,任何ForEachAsync实现都只是语法糖,它隐藏了实现的语义,这就是为什么我倾向于避免使用它。

这给您留下SemaphoreSlimActionBlock的对比。我通常建议人们首先从SemaphoreSlim开始,如果他们的需求变得更加复杂(以某种方式,他们似乎将从数据流管道中受益),考虑考虑使用TPL数据流。

例如,“部分要求是使并行度可配置。”

您可以从允许一定程度的并发开始-被限制的事情是一个整体操作(从url获取数据,将下载的数据反序列化为对象,持久化为crm动态和另一个数据库,然后更新第一个数据库)。这是SemaphoreSlim将是完美解决方案的地方。

但是您可能会决定要使用多个旋钮:例如,对于要下载的URL数量,一个并发程度,对于持久化的单独并发程度,以及对于更新原始数据库的单独的并发程度。然后,您还需要限制这些点之间的“队列”:仅在内存中存储如此多的反序列化对象等,以确保使用慢速数据库的快速url不会导致应用程序使用过多的问题记忆。如果这些是有用的语义,那么您已经开始从数据流的角度解决问题,这就是使用TPL Dataflow之类的库可能会更好的服务点。

答案 1 :(得分:0)

以下是信号量方法的卖点:

  1. 简单性

以下是 TPL Dataflow 方法的卖点:

  1. 基于数据并行性的任务并行性
  2. 资源(带宽,CPU,数据库连接)的最佳利用
  3. 每个异构操作的可配置并行度
  4. 减少内存占用

例如,让我们回顾以下信号量实现:

string[] urls = FetchUrlsFromDB();
var cts = new CancellationTokenSource();
var semaphore = new SemaphoreSlim(10); // Degree of parallelism (DOP)
Task[] tasks = urls.Select(url => Task.Run(async () =>
{
    await semaphore.WaitAsync(cts.Token);
    try
    {
        string rawData = DownloadData(url);
        var data = Deserialize(rawData);
        PersistToCRM(data);
        MarkAsCompleted(url);
    }
    finally
    {
        semaphore.Release();
    }
})).ToArray();
Task.WaitAll(tasks);

以上实现可确保在任何给定时刻最多同时处理10个url。但是,这些并行工作流之间将没有协调。因此,例如,在给定的时刻,所有10个并行工作流都将下载数据,在另一时刻,所有10个将对原始数据进行反序列化,而在另一时刻,所有10个将数据持久化到CRM,则完全有可能。这远非理想。 理想情况下,您希望使整个操作(无论是网络适配器,CPU还是数据库服务器)的瓶颈始终不间断地工作,并且在各种随机时刻都不会被利用不足(或完全闲置)。 / p>

另一个要考虑的问题是,对于每个异构操作,并行化的最佳程度是多少。 10 DOP对于与Web的通信可能是最佳的,但是对于与数据库的通信来说太低或太高。信号量方法不允许进行这种级别的微调。您唯一的选择是通过在这些最佳值之间的某个位置选择DOP值来妥协。

如果URL的数量很大,可以说是1,000,000,那么上面的Semaphore方法也会引起严重的内存使用注意事项。网址的平均大小可能为50个字节,而连接到Task的{​​{1}}可能重10倍甚至更多。当然,您可以更改实现并以更聪明的方式使用SemaphoreSlim,而不会产生太多任务,但这会违背该方法的主要(唯一)卖点,即简单性。 / p>

TPL Dataflow库解决了所有这些问题,但付出的代价是(小的)学习曲线,以便能够驯服这个强大的工具。