繁体   English   中英

TPL任务+动态== OutOfMemoryException?

[英]TPL Tasks + dynamic == OutOfMemoryException?

我正在开发一个流媒体推特客户端 - 经过1-2天的持续运行后,我的内存使用量超过了1.4gig(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。

Eric Lippert(微软)的这个答案似乎表明,即使我的代码中没有任何引用,我也会在幕后解析对象,而这些对象并不会得到GC。

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

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

谢谢

更新:

12年12月14日:

答案

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

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

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

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

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

12年10月17日:

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

与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 (单线程执行)会产生稳定的内存。

不过,你给出的副本似乎有点做作。 如果确实存在太多并发任务,则可以尝试使用限制并发调度自定义任务调度程序

这里的问题不在于您正在创建的任务没有被清理。 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个任务。 当我运行它时,内存是稳定的(平均时间为几秒)。 它通过创建固定数字来限制任务,然后设置延续以在现有任务完成时安排新任务。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM