Condividi tramite


Come scrivere un semplice ciclo Parallel.For

Questo argomento contiene due esempi che illustrano il metodo Parallel.For. Il primo usa l'overload del metodo Parallel.For(Int64, Int64, Action<Int64>) e il secondo usa l'overload Parallel.For(Int32, Int32, Action<Int32>), 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 del 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 di dimensioni della directory

Questo esempio è una semplice utilità della riga di comando che calcola le dimensioni totali dei file in una directory. Prevede un percorso di directory singolo come argomento e segnala il numero e le dimensioni totali dei file in tale directory. Dopo aver verificato che la directory esista, usa il metodo Parallel.For per enumerare i file nella directory e determinarne le dimensioni. Ogni dimensione del file viene quindi aggiunta alla variabile totalSize. Si noti che l'aggiunta viene eseguita chiamando il Interlocked.Add in modo che l'addizione venga eseguita come operazione atomica. In caso contrario, più attività potrebbero provare ad 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 '{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 di matrice e cronometro

In questo esempio viene usato il metodo Parallel.For per calcolare il prodotto di due matrici. Viene inoltre illustrato come usare la classe System.Diagnostics.Stopwatch per confrontare le prestazioni di un ciclo parallelo con un ciclo non parallelo. Si noti che, poiché può generare un volume elevato di output, l'esempio consente di reindirizzare l'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 {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 esegue la parallelizzazione di qualsiasi codice, inclusi i cicli, un obiettivo importante è usare i processori il più possibile senza parallelizzare fino al punto in cui il sovraccarico per l'elaborazione parallela nega i vantaggi delle prestazioni. In questo particolare esempio, solo il ciclo esterno viene parallelizzato perché non è presente molto lavoro eseguito nel ciclo interno. La combinazione di una piccola quantità di lavoro e effetti indesiderati della cache può comportare una riduzione delle prestazioni nei cicli paralleli annidati. Di conseguenza, la parallelizzazione del ciclo esterno è il modo migliore per massimizzare i vantaggi della concorrenza nella 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, indipendentemente dal fatto che abbia zero, uno o sedici parametri di tipo, restituisce sempre void. In Visual Basic il comportamento di un Action viene definito con un Sub. Nell'esempio viene usata un'espressione lambda per creare il delegato, ma è anche possibile creare il delegato 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 relativo valore iniziale è l'indice del primo elemento nel segmento (partizione) dell'origine in fase di elaborazione nel thread corrente.

Se è necessario un maggiore controllo sul livello di concorrenza, usare uno degli overload che accetta un parametro di input System.Threading.Tasks.ParallelOptions, ad esempio: Parallel.For(Int32, Int32, ParallelOptions, Action<Int32,ParallelLoopState>).

Valore restituito e gestione delle eccezioni

For restituisce un oggetto System.Threading.Tasks.ParallelLoopResult quando tutti i thread hanno completato. Questo valore restituito è utile quando si arresta o si interrompe manualmente il ciclo, perché il ParallelLoopResult archivia informazioni come l'ultima iterazione eseguita fino al completamento. Se si verificano una o più eccezioni in uno dei thread, verrà generata una System.AggregateException.

Nel codice in questo esempio il valore restituito di For non viene usato.

Analisi e prestazioni

È possibile usare la Procedura guidata prestazioni per visualizzare l'utilizzo della CPU nel computer. Come esperimento, aumentare il numero di colonne e righe nelle matrici. Più grandi sono le matrici, maggiore è la differenza di prestazioni tra le versioni parallele e sequenziali del calcolo. Quando la matrice è piccola, la versione sequenziale verrà eseguita più velocemente a causa del sovraccarico nella configurazione del ciclo parallelo.

Le chiamate sincrone alle risorse condivise, ad esempio la console o il file system, riducono significativamente le prestazioni di un ciclo parallelo. Quando si misurano le prestazioni, cercare di evitare chiamate come Console.WriteLine all'interno del ciclo.

Compilare il codice

Copiare e incollare questo codice in un progetto di Visual Studio.

Vedere anche