簡體   English   中英

MaxDegreeOfParallelism 有什么作用?

[英]What does MaxDegreeOfParallelism do?

我正在使用Parallel.ForEach並且我正在做一些數據庫更新,現在沒有設置MaxDegreeOfParallelism ,雙核處理器機器導致 SQL 客戶端超時,而四核處理器機器不知何故不會超時。

現在我無法控制我的代碼運行時可用的處理器內核類型,但是是否可以使用MaxDegreeOfParallelism更改某些設置,這些設置可能會同時運行較少的操作並且不會導致超時?

我可以增加超時,但這不是一個好的解決方案,如果在較低的 CPU 上我可以同時處理較少的操作,那將減少 cpu 的負載。

好的,我也閱讀了所有其他帖子和 MSDN,但是將MaxDegreeOfParallelism設置為較低的值會使我的四核機器受到影響嗎?

例如,如果 CPU 有兩個內核,則使用 20,如果 CPU 有四個內核,則使用 40?

答案是它是整個並行操作的上限,與核心數無關。

因此,即使您因為等待IO或鎖定而不使用CPU,也不會並行運行額外的任務,只會指定您指定的最大值。

為了找到這個,我寫了這段測試代碼。 那里有一個人工鎖,可以刺激TPL使用更多的線程。 當您的代碼等待IO或數據庫時,也會發生同樣的情況。

class Program
{
    static void Main(string[] args)
    {
        var locker = new Object();
        int count = 0;
        Parallel.For
            (0
             , 1000
             , new ParallelOptions { MaxDegreeOfParallelism = 2 }
             , (i) =>
                   {
                       Interlocked.Increment(ref count);
                       lock (locker)
                       {
                           Console.WriteLine("Number of active threads:" + count);
                           Thread.Sleep(10);
                        }
                        Interlocked.Decrement(ref count);
                    }
            );
    }
}

如果我沒有指定MaxDegreeOfParallelism,則控制台日志記錄顯示最多同時運行大約8個任務。 像這樣:

Number of active threads:6
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:6
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7
Number of active threads:7

它開始降低,隨着時間的推移而增加,最后它試圖同時運行8。

如果我將它限制在某個任意值(比如2),我會得到

Number of active threads:2
Number of active threads:1
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2
Number of active threads:2

哦,這是在四核機器上。

例如,無論如何,如果CPU有兩個內核,那么使用20,如果CPU有四個內核,那么40?

您可以執行此操作以使並行性取決於CPU核心數:

var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount * 10 };
Parallel.ForEach(sourceCollection, options, sourceItem =>
{
    // do something
});

但是,較新的CPU傾向於使用超線程來模擬額外的內核。 因此,如果你有一個四核處理器,那么Environment.ProcessorCount可能會將其報告為8個核心。 我發現如果你將並行性設置為模擬核心,那么它實際上會減慢其他線程,如UI線程。

因此,盡管操作完成得更快,但應用程序UI在此期間可能會遇到嚴重延遲。 將“Environment.ProcessorCount”除以2似乎可以實現相同的處理速度,同時仍然保持CPU可用於UI線程。

要考慮的其他事情,特別是對於那些多年后發現的事情,取決於您的情況,通常最好收集DataTable中的所有數據,然后在每個主要任務結束時使用SqlBulkCopy。

例如,我有一個運行數百萬個文件的進程,當每個文件事務進行數據庫查詢以插入記錄時,我遇到了相同的錯誤。 我轉而將其全部存儲在內存中的DataTable中,用於我遍歷的每個共享,將DataTable轉儲到我的SQL Server中並在每個單獨的共享之間清除它。 批量插入需要一瞬間,並且不會同時打開數千個連接。

編輯:這是一個快速和臟的工作示例SQLBulkCopy方法:

private static void updateDatabase(DataTable targetTable)
    {
        try
        {
            DataSet ds = new DataSet("FileFolderAttribute");
            ds.Tables.Add(targetTable);
            writeToLog(targetTable.TableName + " - Rows: " + targetTable.Rows.Count, logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
            writeToLog(@"Opening SQL connection", logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
            Console.WriteLine(@"Opening SQL connection");
            SqlConnection sqlConnection = new SqlConnection(sqlConnectionString);
            sqlConnection.Open();
            SqlBulkCopy bulkCopy = new SqlBulkCopy(sqlConnection, SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.FireTriggers | SqlBulkCopyOptions.UseInternalTransaction, null);
            bulkCopy.DestinationTableName = "FileFolderAttribute";
            writeToLog(@"Copying data to SQL Server table", logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
            Console.WriteLine(@"Copying data to SQL Server table");
            foreach (var table in ds.Tables)
            {
                writeToLog(table.ToString(), logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                Console.WriteLine(table.ToString());
            }
            bulkCopy.WriteToServer(ds.Tables[0]);

            sqlConnection.Close();
            sqlConnection.Dispose();
            writeToLog(@"Closing SQL connection", logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
            writeToLog(@"Clearing local DataTable...", logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
            Console.WriteLine(@"Closing SQL connection");
            Console.WriteLine(@"Clearing local DataTable...");
            targetTable.Clear();
            ds.Tables.Remove(targetTable);
            ds.Clear();
            ds.Dispose();
        }
        catch (Exception error)
        {
            errorLogging(error, getCurrentMethod(), logDatabaseFile);
        }
    }

...並將其轉儲到數據表中:

private static void writeToDataTable(string ServerHostname, string RootDirectory, string RecordType, string Path, string PathDirectory, string PathFileName, string PathFileExtension, decimal SizeBytes, decimal SizeMB, DateTime DateCreated, DateTime DateModified, DateTime DateLastAccessed, string Owner, int PathLength, DateTime RecordWriteDateTime)
    {
        try
        {
            if (tableToggle)
            {
                DataRow toInsert = results_1.NewRow();
                toInsert[0] = ServerHostname;
                toInsert[1] = RootDirectory;
                toInsert[2] = RecordType;
                toInsert[3] = Path;
                toInsert[4] = PathDirectory;
                toInsert[5] = PathFileName;
                toInsert[6] = PathFileExtension;
                toInsert[7] = SizeBytes;
                toInsert[8] = SizeMB;
                toInsert[9] = DateCreated;
                toInsert[10] = DateModified;
                toInsert[11] = DateLastAccessed;
                toInsert[12] = Owner;
                toInsert[13] = PathLength;
                toInsert[14] = RecordWriteDateTime;

                results_1.Rows.Add(toInsert);
            }
            else
            {
                DataRow toInsert = results_2.NewRow();
                toInsert[0] = ServerHostname;
                toInsert[1] = RootDirectory;
                toInsert[2] = RecordType;
                toInsert[3] = Path;
                toInsert[4] = PathDirectory;
                toInsert[5] = PathFileName;
                toInsert[6] = PathFileExtension;
                toInsert[7] = SizeBytes;
                toInsert[8] = SizeMB;
                toInsert[9] = DateCreated;
                toInsert[10] = DateModified;
                toInsert[11] = DateLastAccessed;
                toInsert[12] = Owner;
                toInsert[13] = PathLength;
                toInsert[14] = RecordWriteDateTime;

                results_2.Rows.Add(toInsert);
            }


        }
        catch (Exception error)
        {
            errorLogging(error, getCurrentMethod(), logFile);
        }
    }

......這是上下文,循環片本身:

private static void processTargetDirectory(DirectoryInfo rootDirectory, string targetPathRoot)
    {
        DateTime StartTime = DateTime.Now;
        int directoryCount = 0;
        int fileCount = 0;
        try
        {                
            manageDataTables();

            Console.WriteLine(rootDirectory.FullName);
            writeToLog(@"Working in Directory: " + rootDirectory.FullName, logFile, getLineNumber(), getCurrentMethod(), true);

            applicationsDirectoryCount++;

            // REPORT DIRECTORY INFO //
            string directoryOwner = "";
            try
            {
                directoryOwner = File.GetAccessControl(rootDirectory.FullName).GetOwner(typeof(System.Security.Principal.NTAccount)).ToString();
            }
            catch (Exception error)
            {
                //writeToLog("\t" + rootDirectory.FullName, logExceptionsFile, getLineNumber(), getCurrentMethod(), true);
                writeToLog("[" + error.Message + "] - " + rootDirectory.FullName, logExceptionsFile, getLineNumber(), getCurrentMethod(), true);
                errorLogging(error, getCurrentMethod(), logFile);
                directoryOwner = "SeparatedUser";
            }

            writeToRawLog(serverHostname + "," + targetPathRoot + "," + "Directory" + "," + rootDirectory.Name + "," + rootDirectory.Extension + "," + 0 + "," + 0 + "," + rootDirectory.CreationTime + "," + rootDirectory.LastWriteTime + "," + rootDirectory.LastAccessTime + "," + directoryOwner + "," + rootDirectory.FullName.Length + "," + DateTime.Now + "," + rootDirectory.FullName + "," + "", logResultsFile, true, logFile);
            //writeToDBLog(serverHostname, targetPathRoot, "Directory", rootDirectory.FullName, "", rootDirectory.Name, rootDirectory.Extension, 0, 0, rootDirectory.CreationTime, rootDirectory.LastWriteTime, rootDirectory.LastAccessTime, directoryOwner, rootDirectory.FullName.Length, DateTime.Now);
            writeToDataTable(serverHostname, targetPathRoot, "Directory", rootDirectory.FullName, "", rootDirectory.Name, rootDirectory.Extension, 0, 0, rootDirectory.CreationTime, rootDirectory.LastWriteTime, rootDirectory.LastAccessTime, directoryOwner, rootDirectory.FullName.Length, DateTime.Now);

            if (rootDirectory.GetDirectories().Length > 0)
            {
                Parallel.ForEach(rootDirectory.GetDirectories(), new ParallelOptions { MaxDegreeOfParallelism = directoryDegreeOfParallelism }, dir =>
                {
                    directoryCount++;
                    Interlocked.Increment(ref threadCount);
                    processTargetDirectory(dir, targetPathRoot);
                });

            }

            // REPORT FILE INFO //
            Parallel.ForEach(rootDirectory.GetFiles(), new ParallelOptions { MaxDegreeOfParallelism = fileDegreeOfParallelism }, file =>
            {
                applicationsFileCount++;
                fileCount++;
                Interlocked.Increment(ref threadCount);
                processTargetFile(file, targetPathRoot);
            });

        }
        catch (Exception error)
        {
            writeToLog(error.Message, logExceptionsFile, getLineNumber(), getCurrentMethod(), true);
            errorLogging(error, getCurrentMethod(), logFile);
        }
        finally
        {
            Interlocked.Decrement(ref threadCount);
        }

        DateTime EndTime = DateTime.Now;
        writeToLog(@"Run time for " + rootDirectory.FullName + @" is: " + (EndTime - StartTime).ToString() + @" | File Count: " + fileCount + @", Directory Count: " + directoryCount, logTimingFile, getLineNumber(), getCurrentMethod(), true);
    }

如上所述,這很快,很臟,但效果很好。

對於與內存相關的問題,我遇到了大約2,000,000個記錄,我不得不創建第二個DataTable並在2之間交替,在更改之間將記錄轉儲到SQL Server。 所以我的SQL連接包含每100,000個記錄1個。

我像這樣管理:

private static void manageDataTables()
    {
        try
        {
            Console.WriteLine(@"[Checking datatable size] toggleValue: " + tableToggle + " | " + @"r1: " + results_1.Rows.Count + " - " + @"r2: " + results_2.Rows.Count);
            if (tableToggle)
            {
                int rowCount = 0;
                if (results_1.Rows.Count > datatableRecordCountThreshhold)
                {
                    tableToggle ^= true;
                    writeToLog(@"results_1 row count > 100000 @ " + results_1.Rows.Count, logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                    rowCount = results_1.Rows.Count;
                    logResultsFile = "FileServerReport_Results_" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".txt";
                    Thread.Sleep(5000);
                    if (results_1.Rows.Count != rowCount)
                    {
                        writeToLog(@"results_1 row count increased, @ " + results_1.Rows.Count, logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                        rowCount = results_1.Rows.Count;
                        Thread.Sleep(15000);
                    }
                    writeToLog(@"results_1 row count stopped increasing, updating database...", logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                    updateDatabase(results_1);
                    results_1.Clear();
                    writeToLog(@"results_1 cleared, count: " + results_1.Rows.Count, logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                }

            }
            else
            {
                int rowCount = 0;
                if (results_2.Rows.Count > datatableRecordCountThreshhold)
                {
                    tableToggle ^= true;
                    writeToLog(@"results_2 row count > 100000 @ " + results_2.Rows.Count, logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                    rowCount = results_2.Rows.Count;
                    logResultsFile = "FileServerReport_Results_" + DateTime.Now.ToString("yyyyMMdd-HHmmss") + ".txt";
                    Thread.Sleep(5000);
                    if (results_2.Rows.Count != rowCount)
                    {
                        writeToLog(@"results_2 row count increased, @ " + results_2.Rows.Count, logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                        rowCount = results_2.Rows.Count;
                        Thread.Sleep(15000);
                    }
                    writeToLog(@"results_2 row count stopped increasing, updating database...", logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                    updateDatabase(results_2);
                    results_2.Clear();
                    writeToLog(@"results_2 cleared, count: " + results_2.Rows.Count, logDatabaseFile, getLineNumber(), getCurrentMethod(), true);
                }
            }
        }
        catch (Exception error)
        {
            errorLogging(error, getCurrentMethod(), logDatabaseFile);
        }
    }

其中“datatableRecordCountThreshhold = 100000”

聽起來你並行運行的代碼是死鎖,這意味着除非你能找到並修復造成這個問題的問題,否則你根本不應該並行化它。

Parallel.ForEach方法在內部啟動許多Task ,並且這些任務中的每一個重復地從source序列中獲取一個項目並調用該項目的body委托。 MaxDegreeOfParallelism可以為這些內部任務設置上限。 但是這個設置並不是限制並行度的唯一因素。 TaskScheduler也願意執行由Parallel.ForEach產生的任務。

生成機制通過每個生成的任務復制自身來工作。 換句話說,每個任務做的第一件事就是創建另一個任務。 大多數TaskScheduler對可以並發執行的任務數量有限制,當達到此限制時,它們會將下一個傳入任務排隊,而不是立即執行它們。 因此,最終Parallel.ForEach的自我復制模式將停止生成更多任務,因為生成的最后一個任務將閑置在TaskScheduler的隊列中。

讓我們談談TaskScheduler.Default ,它是Parallel.ForEach的默認調度程序,並在ThreadPool上調度任務。 ThreadPool有軟限制和硬限制。 軟限制是指不能立即滿足工作需求,而硬限制是指在已經運行的工作項完成之前永遠不會滿足工作需求。 ThreadPool達到軟限制(默認情況下為Environment.ProcessorCount )時,它會以每秒一個新線程的頻率生成更多線程來滿足需求¹。 可以使用ThreadPool.SetMinThreads方法配置軟限制。 硬限制可以通過ThreadPool.GetMaxThreads方法找到,在我的機器中是 32,767 個線程。

因此,如果我在我的 4 核機器中配置Parallel.ForEach MaxDegreeOfParallelism = 20 ,並且body委托使當前線程忙碌超過一秒,則有效並行度將從 5 開始,然后逐漸增加接下來的 15 秒,直到它變成 20,它會一直保持在 20,直到循環完成。 它以 5 而不是 4 開頭的原因是因為Parallel.ForEach還使用當前線程以及ThreadPool

如果我不配置MaxDegreeOfParallelism ,它將與配置它的值相同-1 ,這意味着無限並行。 在這種情況下, ThreadPool可用性將是實際並行度的唯一限制因素。 只要Parallel.ForEach在運行, ThreadPool就會飽和,換句話說就是處於供不應求的狀態。 每次ThreadPool產生一個新線程時,該線程將選擇Parallel.ForEach先前安排的最后一個任務,該任務將立即復制自身,副本將進入ThreadPool的隊列。 如果Parallel.ForEach將運行足夠長的時間, ThreadPool將達到其最大大小(在我的機器中為 32,767),並將保持在該級別直到循環完成。 這假設該進程不會因為缺少其他資源(如 memory)而崩潰。

MaxDegreeOfParallelism屬性的官方文檔指出“通常,您不需要修改此設置” 顯然,自從使用 .NET Framework 4.0 (2010) 引入 TPL 以來,情況一直如此。 此時您可能已經開始質疑此建議的有效性。 我也是,所以我在 do.net/runtime 存儲庫上發布了一個問題,詢問給定的建議是否仍然有效或已過時。 我很驚訝地收到反饋,說這個建議一如既往地有效。 微軟的說法是,將MaxDegreeOfParallelism限制為值Environment.ProcessorCount可能會導致性能下降,甚至在某些場景下會出現死鎖。 我用幾個示例作為回應,演示了當未配置的Parallel.ForEach在啟用異步的應用程序中運行時可能出現的問題行為,其中其他事情與並行循環同時發生。 由於我使用了Thread.Sleep方法來模擬循環內的工作,因此這些演示認為是不具代表性的。

我個人的建議是:無論何時使用任何Parallel方法,始終明確指定MaxDegreeOfParallelism 如果您購買我的 arguments 認為飽和ThreadPool是不可取的和不健康的,您可以使用合適的值配置它,例如Environment.ProcessorCount 如果您購買 Microsoft 的 arguments,則可以使用-1對其進行配置。 在任何情況下,每個看到您代碼的人都會被暗示您做出了有意識和明智的決定。

¹未記錄ThreadPool的注入率。 “每秒一個新線程”是一個實驗觀察結果。

它設置並行運行的線程數...

暫無
暫無

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

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