我正在使用一个内存饥饿的应用程序,该应用程序使用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);
}
我不得不在两小时后中止 - 我今晚要让它过夜,看看它是否完成了。内存使用似乎更好,因为它运行但仍然很慢。
答案 0 :(得分:0)
你是对的,问题在那里
foreach (var task in tasks)
{
report.AddRange(task.Result);
}
但问题比你想象的要大得多。每次调用阻止调用线程有效地将代码转换为过度设计的串行版本,其中也有一些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();
}
}