【问题标题】:Reclaiming memory in a multithreaded environment - parallel tasks在多线程环境中回收内存 - 并行任务
【发布时间】:2012-06-25 17:38:12
【问题描述】:

我目前正在开发一个小型模拟实用程序,使用任务并行库来提高生成结果的速度。模拟本身是一项耗时的、占用大量 CPU 的工作,基本上由数千个运行具有不同变量的模拟的小型工作组成。

但是,每个任务使用的资源要等到一切都完成后才会释放,如果使用了足够多的变量,就会导致内存泄漏和内存不足异常。在每个任务结束时强制 GC 会释放资源,但我的理解是这需要中断所有线程才能执行,因此会导致接近单线程性能!

如何在这样的长时间操作中释放资源?

在这种情况下,我指的是双精度数组......其中很多。

public List<AnalysisTask> Questions; //Each variable combination is added as a Q

//Create a task for each simulation
Task<SimulationResults>[] tasks = new Task<SimulationResults>[Questions.Count]; 
foreach(var q in Questions)
{
    AnalysisTask temp = q
    tasks[taskCount] = Task.Factory.StartNew((t) =>
             {
                var result = EvaluateRules(temp);
                if(reults.Value > Leader[0].Value)
                    Leader[0] = result;
                else
                {
                    result.Dispose();
                    //This releases resources but interrupts threads
                    //GC.Collect(2, GCCollectionMode.Forced); 
                    return null;
                }
                return result;

             }
}

//Completion task
Task.Factory.ContinueWhenAll(tasks, (ant) =>
       {
          DoSomethingWithAnswer(Leader[0]);
       }

也许我在设置任务时采取了错误的方法?我将不胜感激任何建议或方向:)

【问题讨论】:

  • 第二段描述了问题和症状。下面给出的所有 3 个答案都有助于改进代码,但主要问题已通过有关任务数组所持有的引用的公认答案得到解决。

标签: c# multithreading memory-leaks garbage-collection task


【解决方案1】:

您当前的实现有几个问题。一种是当与Leader[0] 进行交换时,前一个领导者的引用会丢失,并且永远不会被处置。这可能是您的内存泄漏的根源。第二个是对Leader[0] 的比较和赋值不是原子完成的。可能有这样的事件序列:线程 1 与 Leader[0] 比较并在 result.Value 为 1 时为真,线程 2 与 Leader[0] 比较并在 result.Value 为 2 时为真,线程 2 写入Leader[0],线程 1 写入 Leader[0]。结果是Leader[0] 在最大值为 2 时的值为 1。

因此,如果我们正确处理引用,您可能不需要强制进行垃圾回收。下面的代码通过在修改Leader 并存储对先前Leader[0] 的引用来解决这些问题。然后处置未使用的结果或先前的领导者。大概EvaluateRules 需要一些时间,所以不应该有太多的锁争用。

tasks[taskCount] = Task.Factory.StartNew(() =>
     {
        var result = EvaluateRules(temp);

        var toBeDisposed = result;
        lock(Leader) // should be locking on a private object
        {
           if (result.Value > Leader[0].Value)
           {
             toBeDisposed = Leader[0];
             Leader[0] = result;
           }
        }

        toBeDisposed.Dispose();       

     });

另外,您是否需要从每个任务中返回 result?您似乎只需要 Leader[0] 来完成后续任务。通过返回 result,您存储了一个在任务本身被 gc'd 之前无法 gc'd 的引用。

【讨论】:

    【解决方案2】:

    如果数组是恒定大小,或者可以定义最大大小,或者可以定义一组大小范围,您可以在启动时创建这些数组的池或建立一个大小数组列表池在运行期间。然后就不需要解除分配数组 - 只需将它们重新池化以供以后重复使用。一组 BlockingCollection[sizeRange] 队列可以作为池。

    【讨论】:

    • 我通过引入循环数组来限制数组大小,因此拥有一个数组池看起来是一个可行的解决方案......我会看看这个!
    • 如果有不同大小的池数组,最简单的方法是每个 dataArray 对象都引用自己的池作为私有数据成员。这样,您只需在任何 dataArray 上调用“release()”方法,即可将其重新排队到正确的池中。无论如何调试时,在池的“requeue(thisData)”方法中检查“thisData”是否已经在池中也是值得的。双重发布最终会导致两个不同的线程从池中弹出相同的 dataArray 并带来灾难性的后果 - 我今天在我的嵌入式项目中这样做了:(
    【解决方案3】:

    垃圾收集不会停止您的整个过程。请参阅here 了解更多信息。

    如果您必须调用 GC(或者您的进程终止),并且如果 GC 确实会损害您的性能(您不太可能一直执行 GC),那么您总是可以破坏您的模拟分成几个进程(当然不要每个线程使用一个进程,但是每个 X 线程可以属于一个进程)。

    不过,我不得不承认,您的内存管理可能有问题,但您需要提供更多信息。

    【讨论】:

    • 在您的评论和链接之后,我注意到一个 GC 中断被描述为“暂停仍然很短(例如低于 50 毫秒)”......每个单独的模拟可能需要 2 毫秒才能执行,因此每次调用收集都解释了它的原因产生了这样的影响。我只是真正添加了调用作为识别泄漏的测试,但是每 x 个任务调用它会有所帮助。也许还没有像 Martin James 建议的那样对数组进行池化会算作内存管理有问题。
    • 尝试在每个任务中处理多个模拟。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-19
    • 2020-06-23
    相关资源
    最近更新 更多