【问题标题】:TaskFactory.StartNew -> System.OutOfMemoryExceptionTaskFactory.StartNew -> System.OutOfMemoryException
【发布时间】:2011-06-07 20:49:34
【问题描述】:

大约有 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()

【问题讨论】:

  • 原因很可能是你的内存不足。
  • 也许你的 1k 任务?请发布一些代码。您似乎同时使用了太多任务。
  • 我会说你的内存不足。您必须向我们展示一些实际运行并耗尽内存的代码才能获得帮助。
  • 还有,你有多少内存?
  • 每个线程需要多大的栈?我也想知道TaskScheduler.Default.MaximumConcurrencyLevel 设置为什么。

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


【解决方案1】:

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

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

最简单的解决方案可能是查看您的代码中发生这种无限创建任务的位置,看看您是否可以以与您的解决方案一致的方式进行限制。就像您使用生产者/消费者队列一样,将其设为有界队列。

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

【讨论】:

    【解决方案2】:

    当我尝试测试并行系统的限制时,我自己也遇到了这个问题。 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 );
            }
        }
    }
    

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

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

    【讨论】:

    • 有没有办法可以监控可用线程数是否耗尽?几天后,我的应用程序因该异常而崩溃,我想随着时间的推移监控可用空间是否已满。谢谢。
    【解决方案3】:

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

    我建议在创建时添加TaskCreationOptions.LongRunning 标志。这会让ThreadPool 知道它应该考虑oversubscription的任务。

    来自书Parallel Programming with Microsoft .Net

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

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

    【讨论】:

    • 我同意分析和第二个建议(SetMaxThreads),但我怀疑 LongRunningOption 在这里会有所帮助。它建议调度器(更快)创建更多个线程。
    • @Henk:很好,我会删除那个建议。 Parallel Programming 一书推荐在这类场景中使用它,但它是在详细介绍 Parallel 类的部分中。
    • 我所有的任务都是从设置了 LongRunning 标志开始的。我会尝试其他建议。希望对您有所帮助。
    • ThreadPool.GetMaxThreads的结果如下:最大工作线程数:1023,最大完成端口线程数:1000。SetMaxThreads工作或超过1023会导致线程在队列中等待直到线程池线程可用?
    • @Samuel:您可以尝试 SetMaxThreads(32, 1000) 并删除 LongRunning 选项。略有不同。
    【解决方案4】:

    您可能同时启动了太多任务。

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

    【讨论】:

    • 线程太多:是的,任务太多:不太清楚。 1000 个任务不应该是极端的(就其本身而言)。
    猜你喜欢
    • 2011-03-04
    • 1970-01-01
    • 1970-01-01
    • 2016-08-31
    • 1970-01-01
    • 2011-02-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多