TaskFactory.StartNew - >为System.OutOfMemoryException

时间:2011-06-07 20:49:34

标签: c# multithreading task out-of-memory

大约有1000个任务正在运行,但有时我会收到任务调度程序抛出的以下内存不足异常。 可能是什么原因以及如何避免它。

System.Threading.Tasks.TaskSchedulerException: An exception was thrown by a TaskScheduler. ---> System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Threading.Thread.StartInternal(IPrincipal principal, StackCrawlMark& stackMark)
   at System.Threading.Thread.Start(StackCrawlMark& stackMark)
   at System.Threading.Thread.Start(Object parameter)
   at System.Threading.Tasks.ThreadPoolTaskScheduler.QueueTask(Task task)
   at System.Threading.Tasks.Task.ScheduleAndStart(Boolean needsProtection)
   --- End of inner exception stack trace ---
   at System.Threading.Tasks.Task.ScheduleAndStart(Boolean needsProtection)
   at System.Threading.Tasks.Task.InternalStartNew(Task creatingTask, Object action, Object state, CancellationToken cancellationToken, TaskScheduler scheduler, TaskCreationOptions options, InternalTaskOptions internalOptions, StackCrawlMark& stackMark)
   at System.Threading.Tasks.TaskFactory.StartNew(Action action, CancellationToken cancellationToken, TaskCreationOptions creationOptions, TaskScheduler scheduler)
   at App.StartReadSocketTask()

4 个答案:

答案 0 :(得分:7)

您的(非x64)应用最大内存空间为2GB。每个线程至少需要1 MB,通常在达到1000个线程之前可以预期OOM。

本身的Task类应该解决这个问题(通过使用ThreadPool)。但是当你的任务花费太长时间(> 500毫秒)时,TP将慢慢添加线程,几分钟或更长时间后失败。

最简单的解决方案可能是查看代码中无限制创建任务的代码,并查看是否可以以与解决方案一致的方式进行限制。就像您使用Producer / Consumer Que一样,将其设置为有界队列。

否则,限制MaxThreads,但这是一个生硬的应用程序范围的工具。

答案 1 :(得分:5)

我相信你已经遇到了ThreadPool的一个有趣的部分,它决定添加更多的工作线程,因为你当前的任务正在“挨饿”等待的任务。最终这会导致应用程序内存不足。

我建议在创建时添加TaskCreationOptions.LongRunning标志。这将让ThreadPool知道它应该考虑任务的超额订阅

从书Parallel Programming with Microsoft .Net

  

作为最后一个结果,您可以使用SetMaxThreads方法为ThreadPool类配置工作线程数的上限,通常等于核心数(这是{ {1}}属性)...

同一本书还推荐了以下How to: Create a Task Scheduler that Limits the Degree of Concurrency

答案 2 :(得分:4)

当我正在尝试测试并行系统的极限时,我自己遇到了这个问题。 oleksii的评论是现货(1k线程〜= 1GB的已提交内存)。重要的是要注意,这个内存是保留虚拟地址空间而不是实际“使用”的内存量。当系统无法提交大到足以满足您的请求的连续虚拟地址空间块时,会出现内存不足异常(此处插入“内存碎片”修辞)。如果您在Windows任务管理器中查看有关它死亡的时间的过程,您可能会看到只有80-120mb的“已使用”内存。要查看保留多少虚拟地址空间,请在任务管理器中显示“内存 - 提交大小”列。

为了保持这个简短,我可以通过将构建配置从x86切换到64位来突破~1k线程限制。这会增加可用的虚拟地址空间量(大约)2GB到6TB +(取决于您的操作系统版本),并且我的OutOfMemoryException消失了。

这是我创建的简单程序,它说明了这个工件,确保将其作为x86运行并观察它在1k和1.5k线程之间的某个位置 - 然后切换到64位,它应该运行完成而不会失败。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;

namespace TaskToy
{
    class Program
    {
        static void Main( string[] args )
        {
            List<Task> lTasks = new List<Task>();
            int lWidth = 0;
            for ( int i = 0; i < 5000; i ++ )
            {
                lTasks.Add( new Task( (o) => {

                    Console.WriteLine( "B " + Interlocked.Increment( ref lWidth ) + " tid " + Thread.CurrentThread.ManagedThreadId );
                    Thread.Sleep( 60000 );
                    Console.WriteLine( "E " + Interlocked.Decrement( ref lWidth ) + " tid " + Thread.CurrentThread.ManagedThreadId );
                }, null, TaskCreationOptions.LongRunning ) );
            }

            Parallel.For( 0, lTasks.Count, ( i ) =>
            {
                lTasks[i].Start();
            } );

            Task.WaitAll( lTasks.ToArray() );
            Console.WriteLine( "DONE - press any key..." );
            Console.ReadKey( true );
        }
    }
}

P.S。 'lWidth'变量表示当前的并发级别,即一次实际运行的任务数。

总的来说,这是一个有趣的学术实验,但在运行数千个线程提供有用回报之前可能需要几年时间。可能建议将您旋转的线程数限制为更实用的线程 - 可能比“数千”小一个数量级。

答案 3 :(得分:2)

您可能同时开始执行太多任务。

每个任务都可能是一个单独的线程。 CLR为每个线程分配独立的堆栈内存。我假设一个典型的堆栈需要1024Kb的x64 Windows。只需通过跨越线程,您就可以获得1GB的内存,仅用于线程堆栈。这不包括任何堆内存也不包括大对象堆。此外,您还有其他消耗内存的进程。