我正在尝试使用TPL Dataflow,然后将其移植到我的生产代码中。 生产代码是传统的生产者/消费者系统 - 生产者生成消息(与金融领域相关),消费者处理这些消息。
我感兴趣的是,如果在某些时候生产者的生产速度比消费者能够处理的速度快得多(系统会爆炸,或者会发生什么),那么环境将会保持稳定。更重要的是在这些情况下该怎么做。
因此,为了尝试使用类似的简单应用,我想出了以下内容。
var bufferBlock = new BufferBlock<Item>();
var executiondataflowBlockOptions = new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount
,
BoundedCapacity = 100000
};
var dataFlowLinkOptions = new DataflowLinkOptions
{
PropagateCompletion = true
};
var actionBlock1 = new ActionBlock<Item>(t => ProcessItem(t),
executiondataflowBlockOptions);
bufferBlock.LinkTo(actionBlock1, dataFlowLinkOptions);
for (int i = 0; i < int.MaxValue; i++)
{
bufferBlock.SendAsync(GenerateItem());
}
bufferBlock.Complete();
Console.ReadLine();
Item
是一个非常简单的类
internal class Item
{
public Item(string itemId)
{
ItemId = itemId;
}
public string ItemId { get; }
}
GenerateItem
只是新闻Item
static Item GenerateItem()
{
return new Item(Guid.NewGuid().ToString());
}
现在,为了模仿不那么快消费者 - 我让ProcessItem
代表100ms
。
static async Task ProcessItem(Item item)
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
Console.WriteLine($"Processing #{item.ItemId} item.");
}
执行此操作会导致20秒左右的OOM异常。
然后我继续添加更多的消费者(更多的ActionBlocks,最多10个),这会赢得更多的时间,但最终会导致相同的OOM异常。
我还注意到GC受到巨大压力(VS 2015诊断工具显示GC几乎一直在运行),所以我为Item
引入了对象池(非常简单,实际上是ConcurrentBag存储项) ,但我仍然在同一堵墙上(抛出OOM异常)。
提供内存中的内容的一些细节,为什么它已经用完了。
SingleProducerSingleConsumerQueue+Segment<TplDataFlow.Item>
&amp; ConcurrentQueue+Segment<TplDataFlow.Item>
BufferBlock
的InputBuffer已满Item
秒(计数= 14,562,296)BoundedCapacity
(s)设置ActionBlock
后,其输入缓冲区也接近可配置数字(InputCount = 99,996)为了确保较慢的生产者能够让消费者跟上,我让生产者在迭代之间睡觉:
for (int i = 0; i < int.MaxValue; i++)
{
Thread.Sleep(TimeSpan.FromMilliseconds(50));
bufferBlock.SendAsync(GenerateItem());
}
它工作正常 - 没有异常被抛出,内存使用率一直很低,我不再看到任何GC压力了。
所以我很少有问题
BufferBlock
的内部缓冲区正在快速地填充消息,并且在消费者回来询问消息之前一直保持消息下一条消息作为结果应用程序内存不足(由于填充了BufferBlock
的内部缓冲区) - 您是否同意这一点?我正在使用Microsoft.Tpl.Dataflow
包-version 4.5.24。
.NET 4.5(C#6)。进程是32位。
答案 0 :(得分:6)
您已经很好地确定了问题:BufferBlock
正在填充其输入缓冲区,直到它到达OOM。
要解决此问题,您还应该在缓冲区块中添加BoundedCapacity
选项。这将为您自动限制生产者(不需要生产者中的Thread.Sleep
。)
答案 1 :(得分:1)
以下代码可能存在严重的问题:
for (int i = 0; i < int.MaxValue; i++)
{
bufferBlock.SendAsync(GenerateItem()); // Don't do this!
}
SendAsync
方法返回一个Task
,这在对象内存方面可能比要发送到块的实际项目重得多。在特定示例中,由于BufferBlock
具有无限制的容量,因此返回的任务始终会完成,因此该任务的内存占用量几乎为零(始终返回相同的缓存Task<bool>
实例)。但是,在使用较小的BoundedCapacity
值配置块之后,事情很快就会变得有趣起来(以不愉快的方式)。每次对SendAsync
的调用将很快开始返回不完整的Task
,每次都不同,每个任务的内存占用量约为200字节(如果还使用CancellationToken
参数,则占用300字节)。显然,这将无法很好地扩展。
解决方案是按照预期的方式使用SendAsync
。这意味着应该等待它:
for (int i = 0; i < int.MaxValue; i++)
{
await bufferBlock.SendAsync(GenerateItem()); // It's OK now
}
这样,生产者将被异步阻止,直到块内有足够的空间容纳已发送的项目。希望这是您想要的。否则,如果您不想阻止生产者,请不要使用异步SendAsync
方法,而应使用同步Post
方法:
for (int i = 0; i < int.MaxValue; i++)
{
var item = GenerateItem();
while (true)
{
bool accepted = bufferBlock.Post(item); // Synchronous call
if (accepted) break; // Break the inner loop
if (bufferBlock.Completion.IsCompleted) return; // Break both loops
// Here do other things for a while, before retrying to post the item
}
}