Procedura: scrivere un ciclo Parallel.For semplice
Questo argomento contiene due esempi che mostrano il metodo Parallel.For. Il primo usa l'overload del metodo Parallel.For(Int64, Int64, Action<Int64>), mentre il secondo usa l'overload Parallel.For(Int32, Int32, Action<Int32>), ovvero i due overload più semplici del metodo Parallel.For. È possibile usare questi due overload del metodo Parallel.For quando non è necessario annullare il ciclo, interrompere le iterazioni del ciclo o mantenere qualsiasi stato locale dei thread.
Nota
Questa documentazione usa espressioni lambda per definire delegati in TPL. Se non si ha familiarità con le espressioni lambda in C# o Visual Basic, vedere Espressioni lambda in PLINQ e TPL.
Il primo esempio calcola le dimensioni dei file in una singola directory. Il secondo calcola il prodotto di due matrici.
Esempio relativo alle dimensioni di una directory
Questo esempio è una semplice utilità da riga di comando che calcola le dimensioni totali dei file in una directory. È previsto un singolo percorso di directory come argomento e l'esempio indica il numero e le dimensioni totali dei file presenti nella directory. Dopo aver verificato l'esistenza della directory, l'esempio usa il metodo Parallel.For per enumerare i file nella directory e determinarne le dimensioni. Le dimensioni di ogni file vengono quindi aggiunte alla variabile totalSize
. Tenere presente che l'aggiunta viene eseguita chiamando il metodo Interlocked.Add, in modo che sia un'operazione atomica. In caso contrario, più attività potrebbero tentare di aggiornare la variabile totalSize
contemporaneamente.
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
Esempio relativo alle matrici e a Stopwatch
Questo esempio usa il metodo Parallel.For per calcolare il prodotto di due matrici. L'esempio mostra anche come usare la classe System.Diagnostics.Stopwatch per confrontare le prestazioni di un ciclo parallelo con uno non parallelo. Poiché può generare un volume elevato di output, l'esempio permette il reindirizzamento dell'output a un file.
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
Quando si parallelizza qualsiasi codice, inclusi i cicli, un obiettivo importante consiste nell'utilizzare i processori quanto più possibile senza eseguire una parallelizzazione eccessiva che faccia sì che l'overhead per l'elaborazione parallela vanifichi qualsiasi vantaggio in termini di prestazioni. In questo esempio specifico viene parallelizzato solo il ciclo esterno, perché nel ciclo interno non vengono eseguite molte attività. La combinazione di una quantità ridotta di attività e di effetti della cache indesiderati può provocare un peggioramento delle prestazioni nei cicli paralleli annidati. Di conseguenza, la parallelizzazione del solo ciclo esterno è il modo migliore per ottimizzare i vantaggi della concorrenza sulla maggior parte dei sistemi.
Delegato
Il terzo parametro di questo overload di For è un delegato di tipo Action<int>
in C# o Action(Of Integer)
in Visual Basic. Un delegato Action
, che abbia nessuno, uno o sedici parametri di tipo, restituisce sempre void. In Visual Basic il comportamento di un delegato Action
viene definito con un oggetto Sub
. L'esempio usa un'espressione lambda per creare il delegato, ma è possibile creare il delegato anche in altri modi. Per altre informazioni, vedere Espressioni lambda in PLINQ e TPL.
Valore di iterazione
Il delegato accetta un singolo parametro di input il cui valore è l'iterazione corrente. Questo valore di iterazione viene fornito dal runtime e il suo valore iniziale è l'indice del primo elemento nel segmento (partizione) dell'origine elaborata nel thread corrente.
Se si vuole esercitare maggiore controllo sul livello di concorrenza, usare uno degli overload che accetta un parametro di input System.Threading.Tasks.ParallelOptions, come Parallel.For(Int32, Int32, ParallelOptions, Action<Int32,ParallelLoopState>).
Valore restituito e gestione delle eccezioni
For restituisce un oggetto System.Threading.Tasks.ParallelLoopResult al completamento di tutti i thread. Questo valore restituito è utile quando si arresta o si interrompe l'iterazione del ciclo manualmente, perché l'oggetto ParallelLoopResult archivia informazioni come l'ultima iterazione eseguita fino al completamento. Se si verificano una o più eccezioni in uno dei thread, verrà generato un oggetto System.AggregateException.
Nel codice di questo esempio il valore restituito di For non viene usato.
Analisi e prestazioni
È possibile usare la Creazione guidata sessione di prestazioni per visualizzare l'utilizzo CPU nel computer. Come esperimento, aumentare il numero di colonne e righe nelle matrici. Più grandi sono le matrici, maggiore sarà la differenza in termini di prestazioni tra le versioni parallela e sequenziale del calcolo. Quando la matrice è di dimensioni ridotte, la versione sequenziale verrà eseguita più rapidamente a causa dell'overhead durante la configurazione del ciclo parallelo.
Le chiamate sincrone a risorse condivise, come la console o il file system, riducono significativamente le prestazioni di un ciclo parallelo. Per la misurazione delle prestazioni, provare a evitare chiamate come Console.WriteLine all'interno del ciclo.
Compilare il codice
Copiare e incollare questo codice in un progetto di Visual Studio.