内存饥饿的多线程应用程序

时间:2017-04-20 16:02:26

标签: .net memory-management task

我正在使用一个内存饥饿的应用程序,该应用程序使用Task来并行处理。问题是它会产生大量内存,然后挂起,重载我的16GByte系统直到GC运行。在这一点上,它的表现非常糟糕,可能需要数天才能完成。原始应用程序通常需要30分钟才能运行。这是一个精简版:

class Program
{
    static void Main(string[] args)
    {
        var tasks = new List<Task<string[]>>();
        var report = new List<string>();

        for (int i = 0; i < 2000; i++)
        {
            tasks.Add(Task<string[]>.Factory.StartNew(DummyProcess.Process));
        }

        foreach (var task in tasks)
        {
            report.AddRange(task.Result);
        }

        Console.WriteLine("Press RETURN...");
        Console.ReadLine();
    }
}

这是&#39;处理器&#39;:

public static class DummyProcess
{
    public static string[] Process()
    {
        var result = new List<string>();

        for (int i = 1; i < 10000000; i++)
        {
            result.Add($"This is a dummy string of some length [{i}]");
        }

        var random = new Random();
        var delay = random.Next(100, 300);

        Thread.Sleep(delay);

        return result.ToArray();
    }
}

我相信的问题在于:

foreach (var task in tasks)
{
   report.AddRange(task.Result);
}

这些任务在完成后不会被处理 - 从结果中获取结果(字符串[])然后处理任务的最佳方式是什么?

我试过这个:

foreach (var task in tasks)
{
   report.AddRange(task.Result);
   task.Dispose();
}
但是差别不大。我可能会尝试简单地停止返回的结果,这样就不会保留10到50 MB的巨大字符串(在原始应用程序中)。

编辑:我尝试用以下代码替换代码来读取结果:

while (tasks.Any())
{
    var listCopy = tasks.ToList();

    foreach (var task in listCopy)
    {
        if (task.Wait(0))
        {
            report.AddRange(task.Result);
            tasks.Remove(task);
            task.Dispose();
        }
    }

    Thread.Sleep(300);
}

我不得不在两小时后中止 - 我今晚要让它过夜,看看它是否完成了。内存使用似乎更好,因为它运行但仍然很慢。

2 个答案:

答案 0 :(得分:0)

你是对的,问题在那里

foreach (var task in tasks)
{
   report.AddRange(task.Result);
}

但问题比你想象的要大得多。每次调用enter image description here阻止调用线程有效地将代码转换为过度设计的串行版本,其中也有一些Sleeps,太糟糕了!

我建议您首先将代码转换为并行版本,例如为每项任务添加Result

task.ContinueWith(t => {
    //NOTE1 that t.Result is already ready here
    //NOTE2 you need synchronization for your data structure, mutex or synchronized collection
    report.AddRange(t.Result);
});

一旦完成,我还建议每个任务从任务列表中删除自己,这将让GC尽快收集它并保持沉重的结果,我建议使用显式Dispose作为最后的手段,总之:

task.ContinueWith(t => {
    //NOTE1 that t.Result is already ready here
    //NOTE2 you need synchronization for your data structure, mutex or synchronized collection
    report.AddRange(t.Result);
    //NOTE3 Synchronize access to task list!
    tasks.Remove(t);
});

或者,或者,你可以比基于任务的并行性更高一级,并从头开始应用continuation方法:

ParallelLoopResult result = Parallel.For(0, 2000, ctr => {  
    // NOTE you still need to synchronize access to report
    report.Add(/*get your result*/);
});

解释Parallel:虽然结果是相同的,但这会比任务引入更少的开销,特别是对于像你这样的大型集合(2000项),并导致整体运行时间更快。

答案 1 :(得分:0)

task.Result将保留对结果数组的引用,直到无法再从任何根访问该任务。这意味着所有结果数组都将存在,直到tasks列表超出范围。

此外,您创建了2000个线程,这意味着您最多可以同时等待2000组结果数据。如果您更改为使用者 - 生产者模型并且Environment.ProcessorCount个线程执行一个包含2000个作业的工作队列,那么使用内存“飞行”的内容就会减少。使用TPL Dataflow之类的工具,您可以创建一个管道,该管道具有有限数量的工作人员,并且在前一个工作人员通过链中的下一个链接处理其工作之前,工作人员不会开始新的工作。

    static void Main(string[] args)
    {
        var report = new List<string>();

                                                         //We don't use i because you did not have Process accept a parameter of any kind.
        var producer = new TransformBlock<int, string[]>((i) => DummyProcess.Process(), new ExecutionDataflowBlockOptions {MaxDegreeOfParallelism = Environment.ProcessorCount});

        //Only 20 processed items can be in flight at once, if the queue is full it will block the producer threads which there only is Environment.ProcessorCount of them.
        //Only 1 thread is used for the consumer.
        var consumer = new ActionBlock<string[]>((result) => report.AddRange(result), new ExecutionDataflowBlockOptions{BoundedCapacity = 20});
        producer.LinkTo(consumer, new DataflowLinkOptions {PropagateCompletion = true});

        for (int i = 0; i < 2000; i++)
        {
            //We just add dummy values to queue up 2000 items to be processed.
            producer.Post(i);
        }
        //Signals we are done adding to the producer.
        producer.Complete();

        //Waits for the consumer to finish processing all pending items.
        consumer.Completion.Wait();

        Console.WriteLine("Press RETURN...");
        Console.ReadLine();
    }
}