大约有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()
答案 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的内存,仅用于线程堆栈。这不包括任何堆内存也不包括大对象堆。此外,您还有其他消耗内存的进程。