TPL任务+动态== OutOfMemoryException?

时间:2012-10-17 17:51:26

标签: c# dynamic memory-leaks task-parallel-library expandoobject

我正在开发一个流媒体推特客户端 - 经过1-2天的持续运行后,我的内存使用率为> 1.4gigs(32位进程),不久之后就达到了这个数量,我会得到代码上的内存不足异常基本上是这样的(这段代码在我的机器上错误<30秒):

while (true)
{
  Task.Factory.StartNew(() =>
  {
    dynamic dyn2 = new ExpandoObject();

    //get a ton of text, make the string random 
    //enough to be be interned, for the most part
    dyn2.text = Get500kOfText() + Get500kOfText() + DateTime.Now.ToString() + 
      DateTime.Now.Millisecond.ToString(); 
  });
}

我已经对它进行了分析,这肯定是由于DLR中的类下来(来自内存 - 我这里没有我的详细信息)xxRuntimeBinderxx和xxAggregatexx。

This answer from Eric Lippert (microsoft)似乎表明我正在幕后解析对象,即使我的代码中没有任何引用也没有得到任何参考,也不会得到GC。

如果是这种情况,上面的代码中是否有某种方法可以阻止或减少它?

我的后备是消除动态使用,但我不愿意。

感谢

更新

12年12月14日

答案

获取此特定示例以释放其任务的方法是产生(Thread.Sleep(0)),然后允许GC处理释放的任务。我猜测在这种特殊情况下不允许处理消息/事件循环。

在我使用的实际代码(TPL Dataflow)中, 我没有在块上调用Complete() ,因为它们是作为一个永无止境的数据流 - 只要twitter发送它们,任务就会收到Twitter消息。在这个模型中,从来没有任何理由告诉任何阻止它们已经完成,因为只要应用程序正在运行,它们就不会 BE 完成。

不幸的是,看起来数据流块从未设计为长时间运行或处理无数项目,因为它们实际上保留了对发送到它们的所有内容的引用。如果我错了,请告诉我。

因此,解决方法是定期(根据您的内存使用量 - 我的每100k推特消息)释放块并重新设置它们。

根据这个方案,我的内存消耗量永远不会超过80美分,并且在回收块并强制GC进行测量后,gen2堆会回落到6megs,一切都很好。

12年10月17日

  • “这没有做任何有用的事情”:这个例子仅仅是允许的 你快速生成问题。它从少数人那里归结起来 一百行代码与问题无关。
  • 无限循环创建任务,然后创建对象”:记住 - 这只是快速演示问题 - 实际代码正坐在那里等待更多流数据。另外 - 查看代码 - 所有对象都在Action&lt;&gt;内创建。 lambda在任务中。它超出范围后为什么不被清理(最终)?这个问题也不是因为做得太快 - 实际代码需要超过一天才能达到内存不足的例外 - 这只是让它足够快速地尝试。
  • “保证任务被释放吗?”对象是一个对象,不是吗?我的理解是,调度程序只是在池中使用线程,而它正在执行的lambda将在它完成运行后被丢弃。

2 个答案:

答案 0 :(得分:3)

与DLR相比,这更多地与远远领先于消费者的生产者有关。循环尽可能快地创建任务 - 并且任务不会“立即”启动。 很容易弄清楚可能落后的程度:

        int count = 0;

        new Timer(_ => Console.WriteLine(count), 0, 0, 500);

        while (true)
        {
            Interlocked.Increment(ref count);

            Task.Factory.StartNew(() =>
            {
                dynamic dyn2 = new ExpandoObject();
                dyn2.text = Get500kOfText() + Get500kOfText() + DateTime.Now.ToString() +
                  DateTime.Now.Millisecond.ToString();

                Interlocked.Decrement(ref count);
            });
        }

输出:

324080
751802
1074713
1620403
1997559
2431238

这对于3秒钟的调度来说非常重要。 删除Task.Factory.StartNew(单线程执行)会产生稳定的内存。

但是,你给出的复制品似乎有点做作。 如果太多并发任务确实是您的问题,您可以尝试custom task scheduler that limits concurrent scheduling

答案 1 :(得分:1)

这里的问题不在于您正在创建的任务未被清理。 Asti已经证明您的代码创建任务的速度比处理它们的速度快,因此当您清理已完成任务的内存时,最终仍会耗尽。

你说过:

  在这个例子中放置战略性睡眠仍然会产生内存不足的例外 - 它只需要更长的时间

您尚未显示此代码或任何其他限制并发任务数量的示例。我的猜测是你在某种程度上限制了创造,但创造的速度仍然快于消费速度。这是我自己的有限例子:

int numConcurrentActions = 100000;
BlockingCollection<Task> tasks = new BlockingCollection<Task>();

Action someAction = () =>
{
    dynamic dyn = new System.Dynamic.ExpandoObject();

    dyn.text = Get500kOfText() + Get500kOfText() 
        + DateTime.Now.ToString() + DateTime.Now.Millisecond.ToString();
};

//add a fixed number of tasks
for (int i = 0; i < numConcurrentActions; i++)
{
    tasks.Add(new Task(someAction));
}

//take a task out, set a continuation to add a new one when it finishes, 
//and then start the task.
foreach (Task t in tasks.GetConsumingEnumerable())
{
    t.ContinueWith(_ =>
    {
        tasks.Add(new Task(someAction));
    });
    t.Start();
}

此代码将确保任何时候都不会运行超过100,000个任务。当我运行它时,内存是稳定的(平均时间为几秒)。它通过创建固定数字来限制任务,然后设置延续以在现有任务完成时安排新任务。

所以这告诉我们的是,由于您的真实数据基于来自某个外部来源的Feed,因此您从该Feed获取数据的速度要比处理它的速度快一些。你有几个选择。您可以在项目进入时对项目进行排队,确保当前只能运行有限数量的项目,并在超出容量时抛出请求(或者找到一些其他方式过滤输入以便您不进行全部处理) ,或者您可以获得更好的硬件(或优化您拥有的处理方法),以便您能够比创建请求更快地处理请求。

虽然通常我会说当人们已经“足够快”地运行时,人们倾向于尝试优化代码,但事实上并非如此。你有一个相当硬的基准,你需要击中;你需要比他们进来更快地处理项目。目前你没有达到那个基准(但是因为它在失败之前运行了一段时间,你不应该 远远的。)