C# Parallel.ForEach具有自定义TaskScheduler以防止OutOfMemoryException
我通过Parallel.ForEach处理大小千差万别的PDF文件(简单的2MB到几百MB的高DPI扫描),偶尔会遇到OutOfMemoryException,这是可以理解的,因为该进程是32位的,Parallel.ForEach产生的线程占用了未知数量的内存消耗工作 限制C# Parallel.ForEach具有自定义TaskScheduler以防止OutOfMemoryException,c#,multithreading,parallel-processing,C#,Multithreading,Parallel Processing,我通过Parallel.ForEach处理大小千差万别的PDF文件(简单的2MB到几百MB的高DPI扫描),偶尔会遇到OutOfMemoryException,这是可以理解的,因为该进程是32位的,Parallel.ForEach产生的线程占用了未知数量的内存消耗工作 限制MaxDegreeOfParallelism确实有效,但当有大量(10k+)小PDF可供使用时,吞吐量不足,因为由于所述线程的内存占用较小,可能会有更多线程在工作。这是一个CPU密集的并行进程。ForEach在偶尔遇到一组大型
MaxDegreeOfParallelism
确实有效,但当有大量(10k+)小PDF可供使用时,吞吐量不足,因为由于所述线程的内存占用较小,可能会有更多线程在工作。这是一个CPU密集的并行进程。ForEach在偶尔遇到一组大型PDF并出现OutOfMemoryException之前,很容易达到100%的CPU。运行性能分析器可以对此进行备份
根据我的理解,为Parallel.ForEach使用分区器不会提高我的性能
这导致我使用传递给my Parallel.ForEach的自定义TaskScheduler
和MemoryFailPoint
检查。在它周围搜索,似乎很少有关于创建自定义TaskScheduler
对象的信息
在Stackoverflow的各种答案之间,我创建了自己的TaskScheduler
,并拥有自己的QueueTask
方法:
protected override void QueueTask(Task task)
{
lock (tasks) tasks.AddLast(task);
try
{
using (MemoryFailPoint memFailPoint = new MemoryFailPoint(600))
{
if (runningOrQueuedCount < maxDegreeOfParallelism)
{
runningOrQueuedCount++;
RunTasks();
}
}
}
catch (InsufficientMemoryException e)
{
// somehow return thread to pool?
Console.WriteLine("InsufficientMemoryException");
}
}
受保护的覆盖无效队列任务(任务任务)
{
锁定(任务)任务。添加最后一个(任务);
尝试
{
使用(MemoryFailPoint memFailPoint=新的MemoryFailPoint(600))
{
if(runningOrQueuedCount
虽然try/catch有点昂贵,但我的目标是在可能的最大大小PDF(+一点额外的内存开销)为600MB时捕获抛出OutOfMemoryException。当我发现内存不足异常时,这个解决方案似乎会扼杀试图完成这项工作的线程。有了足够大的PDF,我的代码就变成了一个单线程Parallel.ForEach
在Parallel.ForEach和OutOfMemoryExceptions上的Stackoverflow上发现的其他问题似乎不适合我在线程上动态内存使用情况下实现最大吞吐量的用例,通常只是利用MaxDegreeOfParallelism
作为静态解决方案,例如:
- 当线程通过
检查被拒绝工作时,如何将其返回到线程池中MemoryFailPoint
- 当有空闲内存时,如何/在何处安全地生成新线程以重新开始工作
磁盘上的PDF大小可能不会线性表示内存中的大小,因为光栅化和光栅化图像处理组件依赖于PDF内容 使用来自的
limitedconcurrencylevelstaskscheduler
,我能够进行一个小的调整,以获得我想要的东西。以下是修改后的LimitedConcurrencyLevel TaskScheduler
类的NotifyThreadPoolOfPendingWork
方法:
private void NotifyThreadPoolOfPendingWork()
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
// Note that the current thread is now processing work items.
// This is necessary to enable inlining of tasks into this thread.
_currentThreadIsProcessingItems = true;
try
{
// Process all available items in the queue.
while (true)
{
Task item;
lock (_tasks)
{
// When there are no more items to be processed,
// note that we're done processing, and get out.
if (_tasks.Count == 0)
{
--_delegatesQueuedOrRunning;
break;
}
// Get the next item from the queue
item = _tasks.First.Value;
_tasks.RemoveFirst();
}
// Execute the task we pulled out of the queue
//base.TryExecuteTask(item);
try
{
using (MemoryFailPoint memFailPoint = new MemoryFailPoint(650))
{
base.TryExecuteTask(item);
}
}
catch (InsufficientMemoryException e)
{
Thread.Sleep(500);
lock (_tasks)
{
_tasks.AddLast(item);
}
}
}
}
// We're done processing items on the current thread
finally { _currentThreadIsProcessingItems = false; }
}, null);
}
我们来看看渔获量,但反过来看。我们将要处理的任务添加回任务列表(\u tasks
),这将触发一个事件,以获取一个可用线程来处理该任务。但是我们首先休眠当前线程,以便它不会直接执行任务,并返回失败的内存故障点检查