如何:编写简单的 Parallel.For 循环
本主题包含两个示例,这两个示例阐释了 Parallel.For 方法。 第一个示例使用 Parallel.For(Int64, Int64, Action<Int64>) 方法重载,而第二个示例使用 Parallel.For(Int32, Int32, Action<Int32>) 重载,它们是 Parallel.For 方法最简单的两个重载。 如果不需要取消循环、中断循环迭代或保持任何线程本地状态,则可以使用 Parallel.For 方法的这两个重载。
备注
本文档使用 lambda 表达式在 TPL 中定义委托。 如果不熟悉 C# 或 Visual Basic 中的 lambda 表达式,请参阅 PLINQ 和 TPL 中的 Lambda 表达式。
第一个示例计算单个目录中文件的大小。 第二个示例计算两个矩阵的乘积。
目录大小示例
本示例是一个简单的命令行实用工具,用于计算一个目录中的文件总大小。 它需要将单个目录路径作为参数,并报告该目录中文件的数量和总大小。 在验证目录存在后,它会使用 Parallel.For 方法来枚举目录中的文件并确定其文件大小。 然后,将每个文件大小添加到 totalSize
变量。 请注意,此加法操作是通过调用 Interlocked.Add 来执行的,因此它是作为原子操作来执行的。 否则,多个任务可能会同时尝试更新 totalSize
变量。
C#复制
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
public static void Main(string[] args)
{
long totalSize = 0;
if (args.Length == 0) {
Console.WriteLine("There are no command line arguments.");
return;
}
if (! Directory.Exists(args[0])) {
Console.WriteLine("The directory does not exist.");
return;
}
String[] files = Directory.GetFiles(args[0]);
Parallel.For(0, files.Length,
index => { FileInfo fi = new FileInfo(files[index]);
long size = fi.Length;
Interlocked.Add(ref totalSize, size);
} );
Console.WriteLine("Directory '{0}':", args[0]);
Console.WriteLine("{0:N0} files, {1:N0} bytes", files.Length, totalSize);
}
}
// The example displaysoutput like the following:
// Directory 'c:\windows\':
// 32 files, 6,587,222 bytes
矩阵和秒表示例
本示例使用 Parallel.For 方法来计算两个矩阵的乘积。 它还演示如何使用 System.Diagnostics.Stopwatch 类来比较并行循环和非并行循环的性能。 请注意,由于本示例可能会生成大量输出,因此它允许将输出重定向到一个文件。
C#复制
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
class MultiplyMatrices
{
#region Sequential_Loop
static void MultiplyMatricesSequential(double[,] matA, double[,] matB,
double[,] result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
for (int i = 0; i < matARows; i++)
{
for (int j = 0; j < matBCols; j++)
{
double temp = 0;
for (int k = 0; k < matACols; k++)
{
temp += matA[i, k] * matB[k, j];
}
result[i, j] += temp;
}
}
}
#endregion
#region Parallel_Loop
static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
// A basic matrix multiplication.
// Parallelize the outer loop to partition the source array by rows.
Parallel.For(0, matARows, i =>
{
for (int j = 0; j < matBCols; j++)
{
double temp = 0;
for (int k = 0; k < matACols; k++)
{
temp += matA[i, k] * matB[k, j];
}
result[i, j] = temp;
}
}); // Parallel.For
}
#endregion
#region Main
static void Main(string[] args)
{
// Set up matrices. Use small values to better view
// result matrix. Increase the counts to see greater
// speedup in the parallel loop vs. the sequential loop.
int colCount = 180;
int rowCount = 2000;
int colCount2 = 270;
double[,] m1 = InitializeMatrix(rowCount, colCount);
double[,] m2 = InitializeMatrix(colCount, colCount2);
double[,] result = new double[rowCount, colCount2];
// First do the sequential version.
Console.Error.WriteLine("Executing sequential loop...");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
MultiplyMatricesSequential(m1, m2, result);
stopwatch.Stop();
Console.Error.WriteLine("Sequential loop time in milliseconds: {0}",
stopwatch.ElapsedMilliseconds);
// For the skeptics.
OfferToPrint(rowCount, colCount2, result);
// Reset timer and results matrix.
stopwatch.Reset();
result = new double[rowCount, colCount2];
// Do the parallel loop.
Console.Error.WriteLine("Executing parallel loop...");
stopwatch.Start();
MultiplyMatricesParallel(m1, m2, result);
stopwatch.Stop();
Console.Error.WriteLine("Parallel loop time in milliseconds: {0}",
stopwatch.ElapsedMilliseconds);
OfferToPrint(rowCount, colCount2, result);
// Keep the console window open in debug mode.
Console.Error.WriteLine("Press any key to exit.");
Console.ReadKey();
}
#endregion
#region Helper_Methods
static double[,] InitializeMatrix(int rows, int cols)
{
double[,] matrix = new double[rows, cols];
Random r = new Random();
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
matrix[i, j] = r.Next(100);
}
}
return matrix;
}
private static void OfferToPrint(int rowCount, int colCount, double[,] matrix)
{
Console.Error.Write("Computation complete. Print results (y/n)? ");
char c = Console.ReadKey(true).KeyChar;
Console.Error.WriteLine(c);
if (Char.ToUpperInvariant(c) == 'Y')
{
if (!Console.IsOutputRedirected &&
RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console.WindowWidth = 180;
}
Console.WriteLine();
for (int x = 0; x < rowCount; x++)
{
Console.WriteLine("ROW {0}: ", x);
for (int y = 0; y < colCount; y++)
{
Console.Write("{0:#.##} ", matrix[x, y]);
}
Console.WriteLine();
}
}
}
#endregion
}
在对任何代码(包括循环)进行并行化时,一个重要的目标是利用尽可能多的处理器,而不会过度并行化到并行处理的开销使任何性能优势消耗殆尽的程度。 在本特定示例中,只会对外部循环进行并行化,原因是不会在内部循环中执行太多工作。 少量工作和不良缓存影响的组合可能会导致嵌套并行循环的性能降低。 因此,仅并行化外部循环是在大多数系统上最大程度地发挥并发优势的最佳方式。
委托
For 的此重载的第三个参数是类型为 Action<int>
(C# 中)或 Action(Of Integer)
(Visual Basic 中)的委托。 不管 Action
委托具有零个、一个或十六个类型参数,它都始终返回 void。 在 Visual Basic 中,Action
的行为是用 Sub
定义的。 示例使用 lambda 表达式来创建委托,但也可以用其他方式创建委托。 有关详细信息,请参阅 PLINQ 和 TPL 中的 Lambda 表达式。
迭代值
委托采用其值为当前迭代的单一输入参数。 此迭代值由运行时提供,并且其起始值为正在当前线程上处理的源的片段(分区)上第一个元素的索引。
如果需要更好地控制并发级别,请使用采用 System.Threading.Tasks.ParallelOptions 输入参数的重载之一,例如:Parallel.For(Int32, Int32, ParallelOptions, Action<Int32,ParallelLoopState>)。
返回值和异常处理
当所有线程均已完成时,For 会返回一个 System.Threading.Tasks.ParallelLoopResult 对象。 当手动停止或中断循环迭代时,此返回值特别有用,因为 ParallelLoopResult 存储诸如完成运行的最后一个迭代等信息。 如果某个线程上出现一个或多个异常,则将会引发 System.AggregateException。
在本示例的代码中,未使用 For 的返回值。
分析和性能
可以使用性能向导来查看计算机上的 CPU 使用情况。 进行试验,增加矩阵中的列数和行数。 矩阵越大,并行计算和顺序计算之间的性能差异就越大。 当矩阵很小时,由于设置并行循环时会产生开销,因此顺序计算将运行更快。
同步调用共享资源(如控制台或文件系统)将大幅降低并行循环的性能。 在衡量性能时,请尝试避免在循环内进行诸如 Console.WriteLine 等调用。
编译代码
将此代码复制并粘贴到 Visual Studio 项目中。