繁体   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