4

我正在开发一个流式 Twitter 客户端 - 经过 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。

这个来自 Eric Lippert (microsoft) 的回答似乎表明我在幕后制作表达式解析对象,即使在我的代码中没有保留对任何内容的引用,也不会得到 GC。

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

我的后备是消除动态使用,但我不想这样做。

谢谢

更新:

2012 年 12 月 14 日:

答案

这个特定示例释放其任务的方法是 yield (Thread.Sleep(0)),这将允许 GC 处理释放的任务。我猜在这种特殊情况下不允许处理消息/事件循环。

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

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

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

在这个方案下,我的内存消耗永远不会超过 80megs,并且在回收块并强制 GC 进行良好测量之后,gen2 堆又回到了 6megs,一切都恢复正常了。

2012 年 10 月 17 日:

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

2 回答 2

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

不过,您给出的复制品似乎有点做作。如果太多并发任务确实是您的问题,您可以尝试使用限制并发调度的自定义任务调度程序

于 2012-10-17T18:54:58.880 回答
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 个。当我运行它时,内存是稳定的(当平均数秒时)。它通过创建一个固定数量来限制任务,然后设置一个延续以在现有任务完成时安排新任务。

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

虽然通常我会说人们倾向于在代码已经“足够快”运行时尝试优化代码,但这显然不是你的情况。您需要达到一个相当严格的基准;你需要比它们进来的速度更快地处理项目。目前你没有达到那个基准(但是因为它在失败之前运行了一段时间,你不应该那么远)。

于 2012-10-17T20:28:24.417 回答