Como: Escrever um loop Parallel.For simples
Este tópico contém dois exemplos que ilustram o método Parallel.For. O primeiro usa a sobrecarga do método Parallel.For(Int64, Int64, Action<Int64>) e o segundo usa a sobrecarga Parallel.For(Int32, Int32, Action<Int32>), as duas sobrecargas mais simples do método Parallel.For. Você pode usar essas duas sobrecargas do método Parallel.For quando não for preciso cancelar o loop, dividir as iterações do loop ou manter qualquer estado local do thread.
Observação
Esta documentação usa expressões lambda para definir delegados na TLP. Se você não estiver familiarizado com expressões lambda no C# ou no Visual Basic, veja Expressões lambda em PLINQ e TPL.
O primeiro exemplo calcula o tamanho dos arquivos em um único diretório. O segundo calcula o produto de duas matrizes.
Exemplo de tamanho de diretório
Este exemplo é um utilitário de linha de comando simples que calcula o tamanho total dos arquivos em um diretório. Ele espera um caminho de diretório único como um argumento e informa o número e o tamanho total dos arquivos nesse diretório. Depois de verificar se o diretório existe, ele usa o método Parallel.For para enumerar os arquivos no diretório e determinar os tamanhos dos arquivos. Cada tamanho de arquivo é adicionado à variável totalSize
. Observe que a adição é realizada chamando o Interlocked.Add para que a adição seja realizada como uma operação atômica. Caso contrário, várias tarefas podem tentar atualizar a variável totalSize
simultaneamente.
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
Imports System.IO
Imports System.Threading
Imports System.Threading.Tasks
Module Example
Public Sub Main()
Dim totalSize As Long = 0
Dim args() As String = Environment.GetCommandLineArgs()
If args.Length = 1 Then
Console.WriteLine("There are no command line arguments.")
Return
End If
If Not Directory.Exists(args(1))
Console.WriteLine("The directory does not exist.")
Return
End If
Dim files() As String = Directory.GetFiles(args(1))
Parallel.For(0, files.Length,
Sub(index As Integer)
Dim fi As New FileInfo(files(index))
Dim size As Long = fi.Length
Interlocked.Add(totalSize, size)
End Sub)
Console.WriteLine("Directory '{0}':", args(1))
Console.WriteLine("{0:N0} files, {1:N0} bytes", files.Length, totalSize)
End Sub
End Module
' The example displays output like the following:
' Directory 'c:\windows\':
' 32 files, 6,587,222 bytes
Exemplo de matriz e cronômetro
Este exemplo usa o método Parallel.For para calcular o produto de duas matrizes. Ele também mostra como usar a classe System.Diagnostics.Stopwatch para comparar o desempenho de um loop paralelo com um loop não paralelo. Observe que, como ele pode gerar um grande volume de saída, o exemplo permite que a saída seja redirecionada para um arquivo.
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
}
Imports System.Diagnostics
Imports System.Runtime.InteropServices
Imports System.Threading.Tasks
Module MultiplyMatrices
#Region "Sequential_Loop"
Sub MultiplyMatricesSequential(ByVal matA As Double(,), ByVal matB As Double(,), ByVal result As Double(,))
Dim matACols As Integer = matA.GetLength(1)
Dim matBCols As Integer = matB.GetLength(1)
Dim matARows As Integer = matA.GetLength(0)
For i As Integer = 0 To matARows - 1
For j As Integer = 0 To matBCols - 1
Dim temp As Double = 0
For k As Integer = 0 To matACols - 1
temp += matA(i, k) * matB(k, j)
Next
result(i, j) += temp
Next
Next
End Sub
#End Region
#Region "Parallel_Loop"
Private Sub MultiplyMatricesParallel(ByVal matA As Double(,), ByVal matB As Double(,), ByVal result As Double(,))
Dim matACols As Integer = matA.GetLength(1)
Dim matBCols As Integer = matB.GetLength(1)
Dim matARows As Integer = matA.GetLength(0)
' A basic matrix multiplication.
' Parallelize the outer loop to partition the source array by rows.
Parallel.For(0, matARows, Sub(i)
For j As Integer = 0 To matBCols - 1
Dim temp As Double = 0
For k As Integer = 0 To matACols - 1
temp += matA(i, k) * matB(k, j)
Next
result(i, j) += temp
Next
End Sub)
End Sub
#End Region
#Region "Main"
Sub Main(ByVal args As String())
' 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.
Dim colCount As Integer = 180
Dim rowCount As Integer = 2000
Dim colCount2 As Integer = 270
Dim m1 As Double(,) = InitializeMatrix(rowCount, colCount)
Dim m2 As Double(,) = InitializeMatrix(colCount, colCount2)
Dim result As Double(,) = New Double(rowCount - 1, colCount2 - 1) {}
' First do the sequential version.
Console.Error.WriteLine("Executing sequential loop...")
Dim stopwatch As 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 - 1, colCount2 - 1) {}
' 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()
End Sub
#End Region
#Region "Helper_Methods"
Function InitializeMatrix(ByVal rows As Integer, ByVal cols As Integer) As Double(,)
Dim matrix As Double(,) = New Double(rows - 1, cols - 1) {}
Dim r As New Random()
For i As Integer = 0 To rows - 1
For j As Integer = 0 To cols - 1
matrix(i, j) = r.[Next](100)
Next
Next
Return matrix
End Function
Sub OfferToPrint(ByVal rowCount As Integer, ByVal colCount As Integer, ByVal matrix As Double(,))
Console.Error.Write("Computation complete. Display results (y/n)? ")
Dim c As Char = Console.ReadKey(True).KeyChar
Console.Error.WriteLine(c)
If Char.ToUpperInvariant(c) = "Y"c Then
If Not Console.IsOutputRedirected AndAlso
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) Then Console.WindowWidth = 168
Console.WriteLine()
For x As Integer = 0 To rowCount - 1
Console.WriteLine("ROW {0}: ", x)
For y As Integer = 0 To colCount - 1
Console.Write("{0:#.##} ", matrix(x, y))
Next
Console.WriteLine()
Next
End If
End Sub
#End Region
End Module
Ao paralelizar qualquer código, incluindo loops, um objetivo importante é utilizar os processadores o máximo possível sem paralelizar tanto ao ponto da sobrecarga do processamento paralelo prejudicar os benefícios do desempenho. Nesse exemplo específico, somente o loop externo é paralelizado porque não há muito trabalho realizado no loop interno. A combinação de uma pequena quantidade de trabalho e efeitos de cache indesejáveis podem levar à degradação do desempenho em loops paralelos aninhados. Portanto, a paralelização do loop externo é apenas a melhor forma de maximizar os benefícios de simultaneidade na maioria dos sistemas.
O representante
O terceiro parâmetro dessa sobrecarga de For é um representante do tipo Action<int>
em C# ou Action(Of Integer)
no Visual Basic. Um representante de Action
, se tiver zero, um ou dezesseis parâmetros de tipo, sempre retornará nulo. No Visual Basic, o comportamento de um Action
é definido com um Sub
. O exemplo usa uma expressão lambda para criar o representante, mas ele também pode ser criado de outras formas. Para saber mais, confira Expressões lambda em PLINQ e TPL.
O valor da iteração
O representante usa um único parâmetro de entrada cujo valor é a iteração atual. Esse valor da iteração é fornecido pelo runtime e seu valor inicial é o índice do primeiro elemento no segmento (partição) da origem que está sendo processada no thread atual.
Se você precisar de mais controle sobre o nível de simultaneidade, use uma das sobrecargas que usa um parâmetro de entrada System.Threading.Tasks.ParallelOptions, como: Parallel.For(Int32, Int32, ParallelOptions, Action<Int32,ParallelLoopState>).
Valor de retorno e tratamento de exceções
Retornos For usam um objeto System.Threading.Tasks.ParallelLoopResult quando todos os threads estão concluídos. Esse valor de retorno é útil se você estiver parando ou interrompendo a iteração da loop manualmente, já que o ParallelLoopResult armazena informações como a última iteração executada antes da conclusão. Se uma ou mais exceções ocorrerem em um dos threads, um System.AggregateException será lançado.
No código deste exemplo, o valor de retorno de For não é usado.
Análise e desempenho
Você pode usar o Assistente de Desempenho para exibir o uso da CPU no computador. Para experimentar, aumente o número de colunas e linhas de matrizes. Quanto maior as matrizes, maior será a diferença de desempenho entre as versões paralelas e sequenciais da computação. Quando a matriz é pequena, a versão sequencial será executada com maior rapidez devido à sobrecarga da configuração do loop paralelo.
As chamadas síncronas para recursos compartilhados, como o Console ou o Sistema de arquivos, prejudicará significativamente o desempenho de um loop paralelo. Ao medir o desempenho, tente evitar chamadas como Console.WriteLine dentro do loop.
Compile o código
Copie e cole esse código em um projeto do Visual Studio.