SelectMany使用ReactiveExtensions占用大量内存

时间:2016-05-06 09:15:44

标签: c# .net system.reactive reactive-programming

我想创建一个获取图像并返回一些派生对象的管道。

我使用了一系列位图,并为每个位图执行任务(即异步)。所以它看起来很简单。但是,我发现内存消耗非常高。为了说明问题,我已经创建了您可以运行的测试。

请查看内存,因为它最多需要400 MB的RAM。

我可以做些什么来避免记忆太多?这里发生了什么?

[Fact]
public async Task BitmapPipelineTest()
{
    var bitmaps = Enumerable.Range(0, 100).Select(_ => new WriteableBitmap(800, 600, 96, 96, PixelFormats.Bgr24, new BitmapPalette(new List<Color>() { new Color() })));
    var bitmapsObs = bitmaps.ToObservable();

    var processed = bitmapsObs.SelectMany(bitmap => DoSomethingAsync(bitmap));
    processed.Subscribe();

    await Task.Delay(20000);
}

private async Task<object> DoSomethingAsync(BitmapSource bitmap)
{
    await Task.Delay(1000);
    return new object();
}

2 个答案:

答案 0 :(得分:2)

所以我认为这个问题不一定是SelectMany甚至是被动扩展所致。看起来WriteableBitmap使用非托管内存:source code。我相信问题在于,您正在快速连续地创建一堆相对较小的托管对象,这些对象占用了大量非托管内存。来自MSDN

  

如果小型托管对象分配大量非托管内存,则运行时仅考虑托管内存,因此低估了调度垃圾回收的紧迫性。

但是我们可以使用GC.AddMemoryPressureGC.RemoveMemoryPressure函数来提供垃圾收集器提示。这将有助于GC改进其日程安排。在我们能够做到这一点之前,我们需要了解分配的非托管内存量。我相信非托管内存用于存储像素阵列,所以我认为一个好的估计是像素宽度乘以像素高度乘以每个通道中的位数乘以通道数。从MSDN看起来每个通道有32位(4个字节)和4个通道。

我使用类似于以下的代码运行了一些测试并获得了非常好的结果:

var processed = 
    Enumerable
    .Range(0, 100)
    .Select(_ => new WriteableBitmap(
        800, 
        600, 
        96, 
        96, 
        PixelFormats.Bgr24, 
        new BitmapPalette(new List<Color>() { new Color() })))
    .Select(x => new { Bitmap = x, ByteSize = x.PixelWidth * x.PixelHeight * 4 * 4)
    .ToObservable()
    .Do(x => GC.AddMemoryPressure(x.ByteSize))
    .SelectMany(x => DoSomethingAsync(x.Bitmap));

processed
.Subscribe(x => GC.RemoveMemoryPressure(x.ByteSize));

但是,如果您的源发布比您处理它们的位图更快,那么您仍然会遇到问题。背压将导致内存分配的速度快于可以解除分配的速度。

老实说,你真的把位图推给了你吗?我不知道你的实际程序是什么样的,但在你的示例代码中显然是一个基于拉的系统。如果是基于拉动的系统,您考虑过PLINQ吗? PLINQ非常适合这种类型的东西;它可以很好地控制并发性,你不必担心背压。

答案 1 :(得分:1)

在我看来,您遇到了一个简单的内存使用问题。

如果每个通道有4个字节,每个像素有4个通道,则每个800 x 600的1000个图像为1000 x 800 x 600 x 4 x 4 = 733MB(约)。

但令我感到震惊的是,在您的代码中可能会让您感到悲伤的是,您从一个可枚举开始,然后将其转换为一个可观察的,它是使用任务构建的,最终,您可以异步运行着火并忘记await Task.Delay(20000);,你用public async Task BitmapPipelineTest() { await Observable .Range(0, 100) .Select(_ => new WriteableBitmap( 800, 600, 96, 96, PixelFormats.Bgr24, new BitmapPalette(new List<Color>() { new Color() }))) .SelectMany(x => Observable .Start(() => { Thread.Sleep(10); return new object(); })); } 捏造回报。这都容易出错。你应该避免混合你的“monad”。

以下是我的写作方式:

box-sizing