簡體   English   中英

Parallel.ForEach使用自定義TaskScheduler來防止OutOfMemoryException

[英]Parallel.ForEach with a custom TaskScheduler to prevent OutOfMemoryException

我正在通過Parallel.ForEach處理各種大小的PDF(簡單的2MB到幾百MB的高DPI掃描)並且偶爾會遇到OutOfMemoryException - 可以理解的是由於進程是32位並且Parallel產生了線程。 ForEach占用了大量未知的內存消耗工作。

限制MaxDegreeOfParallelism確實有效,盡管由於所述線程的內存占用量較小而導致有大量(10k +)批量的小型PDF需要處理時的吞吐量不足。 這是一個CPU繁重的過程,Parallel.ForEach很容易達到100%的CPU,然后點擊偶爾的一組大型PDF並獲得OutOfMemoryException。 運行Performance Profiler會將其備份。

根據我的理解,為Parallel.ForEach設置分區器不會提高我的性能。

這導致我使用傳遞給我的Parallel.ForEach的自定義TaskSchedulerMemoryFailPoint檢查。 在它周圍搜索似乎有關於創建自定義TaskScheduler對象的稀缺信息。

查看.NET 4 Parallel Extensions Extras中的Specialized Task Scheduler, C#中的自定義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");
    }
}

雖然try / catch有點貴,但我的目標是捕獲600MB的可能最大大小PDF(+一點額外內存開銷)將拋出OutOfMemoryException。 當我捕獲InsufficientMemoryException時,這個解決方案似乎殺掉了試圖完成工作的線程。 有了足夠大的PDF,我的代碼最終成為一個單一的線程Parallel.ForEach。

在Parallel.ForEach和OutOfMemoryExceptions上的Stackoverflow上發現的其他問題似乎不適合我在線程上使用動態內存的最大吞吐量的用例,並且通常只是利用MaxDegreeOfParallelism作為靜態解決方案,例如:

因此,要獲得可變工作內存大小的最大吞吐量,可以:

  • 當一個線程被拒絕通過MemoryFailPoint檢查工作時,如何將線程返回到線程MemoryFailPoint
  • 當有空閑內存時,我如何/在哪里安全地生成新線程以重新開始工作?

編輯:由於光柵化和光柵化圖像處理組件(取決於PDF內容),磁盤上的PDF大小可能不會線性表示內存中的大小。

使用來自.NET Framework並行編程示例的 LimitedConcurrencyLevelTaskScheduler ,我能夠進行微調,以獲得看起來我想要的內容。 以下是NotifyThreadPoolOfPendingWork所述的方法LimitedConcurrencyLevelTaskScheduler改性之后類:

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);
}

我們將看看這個問題,但反之亦然。 我們將我們要處理的任務添加回任務列表( _tasks ),該任務觸發事件以獲得可用線程來獲取該工作。 但是我們首先睡眠當前線程,以便它不直接接收工作並返回失敗的MemoryFailPoint檢查。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM