C# 如何跨多个TPL数据流块跨越MaxDegreeOfParallelism?
我想将所有数据流块中提交给数据库服务器的查询总数限制为30。在下面的场景中,每个块限制30个并发任务,因此在执行过程中始终会影响60个并发任务。显然,我可以将并行性限制在每个块15个,以实现系统范围内总共30个,但这不是最优的 我该怎么做?我是使用信号量lim等限制(并阻止)我的等待,还是有一种内在的数据流方法工作得更好C# 如何跨多个TPL数据流块跨越MaxDegreeOfParallelism?,c#,concurrency,async-await,task-parallel-library,tpl-dataflow,C#,Concurrency,Async Await,Task Parallel Library,Tpl Dataflow,我想将所有数据流块中提交给数据库服务器的查询总数限制为30。在下面的场景中,每个块限制30个并发任务,因此在执行过程中始终会影响60个并发任务。显然,我可以将并行性限制在每个块15个,以实现系统范围内总共30个,但这不是最优的 我该怎么做?我是使用信号量lim等限制(并阻止)我的等待,还是有一种内在的数据流方法工作得更好 public class TPLTest { private long AsyncCount = 0; private long MaxAsyncCount =
public class TPLTest
{
private long AsyncCount = 0;
private long MaxAsyncCount = 0;
private long TaskId = 0;
private object MetricsLock = new object();
public async Task Start()
{
ExecutionDataflowBlockOptions execOption = new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 30 };
DataflowLinkOptions linkOption = new DataflowLinkOptions() { PropagateCompletion = true };
var doFirstIOWorkAsync = new TransformBlock<Data, Data>(async data => await DoIOBoundWorkAsync(data), execOption);
var doCPUWork = new TransformBlock<Data, Data>(data => DoCPUBoundWork(data));
var doSecondIOWorkAsync = new TransformBlock<Data, Data>(async data => await DoIOBoundWorkAsync(data), execOption);
var doProcess = new TransformBlock<Data, string>(i => $"Task finished, ID = : {i.TaskId}");
var doPrint = new ActionBlock<string>(s => Debug.WriteLine(s));
doFirstIOWorkAsync.LinkTo(doCPUWork, linkOption);
doCPUWork.LinkTo(doSecondIOWorkAsync, linkOption);
doSecondIOWorkAsync.LinkTo(doProcess, linkOption);
doProcess.LinkTo(doPrint, linkOption);
int taskCount = 150;
for (int i = 0; i < taskCount; i++)
{
await doFirstIOWorkAsync.SendAsync(new Data() { Delay = 2500 });
}
doFirstIOWorkAsync.Complete();
await doPrint.Completion;
Debug.WriteLine("Max concurrent tasks: " + MaxAsyncCount.ToString());
}
private async Task<Data> DoIOBoundWorkAsync(Data data)
{
lock(MetricsLock)
{
AsyncCount++;
if (AsyncCount > MaxAsyncCount)
MaxAsyncCount = AsyncCount;
}
if (data.TaskId <= 0)
data.TaskId = Interlocked.Increment(ref TaskId);
await Task.Delay(data.Delay);
lock (MetricsLock)
AsyncCount--;
return data;
}
private Data DoCPUBoundWork(Data data)
{
data.Step = 1;
return data;
}
}
起点:
TPLTest tpl = new TPLTest();
await tpl.Start();
为什么不将所有内容封送到具有实际限制的操作块
var count = 0;
var ab1 = new TransformBlock<int, string>(l => $"1:{l}");
var ab2 = new TransformBlock<int, string>(l => $"2:{l}");
var doPrint = new ActionBlock<string>(
async s =>
{
var c = Interlocked.Increment(ref count);
Console.WriteLine($"{c}:{s}");
await Task.Delay(5);
Interlocked.Decrement(ref count);
},
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 15 });
ab1.LinkTo(doPrint);
ab2.LinkTo(doPrint);
for (var i = 100; i > 0; i--)
{
if (i % 3 == 0) await ab1.SendAsync(i);
if (i % 5 == 0) await ab2.SendAsync(i);
}
ab1.Complete();
ab2.Complete();
await ab1.Completion;
await ab2.Completion;
var计数=0;
var ab1=新的TransformBlock(l=>$“1:{l}”);
var ab2=新的TransformBlock(l=>$“2:{l}”);
var doPrint=新动作块(
异步s=>
{
var c=联锁增量(参考计数);
Console.WriteLine($“{c}:{s}”);
等待任务。延迟(5);
联锁。减量(参考计数);
},
新的ExecutionDataflowBlockOptions{MaxDegreeOfParallelism=15});
ab1.LinkTo(doPrint);
ab2.LinkTo(doPrint);
对于(变量i=100;i>0;i--)
{
如果(i%3==0)等待ab1.SendAsync(i);
如果(i%5==0)等待ab2.SendAsync(i);
}
ab1.Complete();
ab2.Complete();
等待ab1.完成;
等待ab2.完成;
这是我最终采用的解决方案(除非我能想出如何使用单个通用数据流块来编组每种类型的数据库访问):
我在类级别定义了一个信号量lim:
private SemaphoreSlim ThrottleDatabaseQuerySemaphore = new SemaphoreSlim(30, 30);
我修改了I/O类以调用节流类:
private async Task<Data> DoIOBoundWorkAsync(Data data)
{
if (data.TaskId <= 0)
data.TaskId = Interlocked.Increment(ref TaskId);
Task t = Task.Delay(data.Delay); ;
await ThrottleDatabaseQueryAsync(t);
return data;
}
使用信号量可能更好,因为它允许您仅锁定每个查询的执行,而不锁定该块中涉及的任何其他工作。创建一个用于运行查询的抽象(或者修改现有的,如果有的话),这样在查询时抽象就去掉了信号量,这样就不必每次都手动执行。我同意@Servy。我建议
SemaphoreSlim
限制对数据库的访问。通过这种方式,您将拥有对数据库访问方式的单点控制,而不依赖于数据流中的细节。与其尝试限制所有块,为什么不使用一个块写入数据库?并行数据库访问并不比并行磁盘访问好——您仍然要写入相同的存储,因此更多的连接会导致性能下降。规范的方法(即在SSI和所有ETL工具中使用)是将数据批处理成足够大的块,然后使用任何可用的批量导入功能将其发送到数据库,例如SqlBulkCopy
您的块在做什么,为什么它们使用DOP为30?这很重要。与其让30个任务尝试在30个连接上插入单行,不如将记录发送到batchblock,然后将该批发送到带有SqlBulkCopy实例的ActionBlock,该实例使用模拟日志记录和无交叉连接阻塞将记录泵送到目标表。像这样的单个任务很容易以30倍的速度结束如果任何块执行查找,最好像SSI一样预加载和缓存查找值。如果块执行多个活动,将它们分开,允许每个块一次执行一项工作,而不会阻塞太长时间。谁否决了这个,为什么?这实际上是处理ETL的正确方法。30个阻塞连接,即使有信号量,也远比1个阻塞泵送数据的速度差,因为它可以提供一个弱示例。我的I/O块将前一个I/O块的输出作为输入,并进行一些小的处理。这些街区实际上并不相同。有些执行查找,有些检索数据集,有些执行存储过程。我可以使用一个返回数据集的泛型块来使用这种方法,但不是针对我所有的I/O块类型。@NPCampbell,这应该不是问题。doPrint
块除了限制同时调用的次数之外,可能什么也不做。这也是一个显示解决方案路径的弱示例。@PauloMorgado我对您的doPrint块注释感兴趣。我有大约10000个元素通过我的8个I/O块的管道运行。我的理解是,伪doPrint块可以限制管道中通过该点的元素数量,但它不会限制限制块上游或下游的数据库活动。我的理解正确吗?它将限制下游的一切,因为只有那个数量的操作将被执行。我的意思是“dumb”不是“什么都不做”,而是“不做任何事情”。这将把数据库并发性问题转移到客户端。这并不能解决问题。使用“通用”数据导入步骤很容易——创建一个ActionBlock,该ActionBlock接受一个记录数组,并将其写入一个具有SqlBulkCopy类的表中。为每个目标表创建一个这样的块以保持简单。在每个批处理记录之前添加一个批处理块,以将记录批处理到例如5000或10000数组中。更高级的版本是使用自定义块将记录一起批处理,并将它们添加到包含记录和目标表名称的DTO中。将它们传递给使用表名配置SqlBulkCopy实例并写出记录的ActionBlock。这将为您提供一个db writer,但“分组”块必须处理每个目标表的不同批量大小,以考虑快表和慢表。此外,数据流、反应式扩展和任务可以组合在一起。源块和目标块可以是可观察和观察者。您可以使用Rx操作,如groupby
和Buffer
,按目标分离记录,并按计数和时间批处理这些记录。由于我的应用程序的行为类似于拒绝服务攻击,因此我不得不在客户端限制它们。我目前在输入端批处理记录,以防止在处理过程中内存不足。80%的性能瓶颈都在
private async Task<Data> DoIOBoundWorkAsync(Data data)
{
if (data.TaskId <= 0)
data.TaskId = Interlocked.Increment(ref TaskId);
Task t = Task.Delay(data.Delay); ;
await ThrottleDatabaseQueryAsync(t);
return data;
}
private async Task ThrottleDatabaseQueryAsync(Task task)
{
await ThrottleDatabaseQuerySemaphore.WaitAsync();
try
{
lock (MetricsLock)
{
AsyncCount++;
if (AsyncCount > MaxAsyncCount)
MaxAsyncCount = AsyncCount;
}
await task;
}
finally
{
ThrottleDatabaseQuerySemaphore.Release();
lock (MetricsLock)
AsyncCount--;
}
}
}