Ausführen asynchroner Aufgaben parallel
Parallele Programmierung ist eine leistungsstarke Technik, mit der Sie mehrere Aufgaben gleichzeitig ausführen und die Leistung und Reaktionsfähigkeit Ihrer Anwendungen verbessern können.
In C# können Sie die Task Parallel Library (TPL) verwenden, um den Prozess des Schreibens parallelen Codes zu vereinfachen. Die TPL ist eine Reihe öffentlicher Typen und APIs in den System.Threading Namespaces System.Threading.Tasks . Der Zweck der TPL besteht darin, Entwickler produktiver zu machen, indem der Prozess, Parallelität und Nebenläufigkeit zu Anwendungen hinzuzufügen, vereinfacht wird. Die TPL skaliert dynamisch den Grad der Parallelität, um alle verfügbaren Prozessoren am effizientesten zu verwenden. Darüber hinaus behandelt die TPL die Partitionierung der Arbeit, die Planung von Threads im ThreadPool, die Abbruchunterstützung, die Zustandsverwaltung und andere Details auf niedriger Ebene. Mithilfe von TPL können Sie die Leistung Ihres Codes maximieren und sich dabei auf die Arbeit konzentrieren, die Ihr Programm zu erreichen hat.
Die TPL bietet Unterstützung in den folgenden Bereichen:
- Datenparallelität: Die TPL stellt Methoden zum Ausführen von Datenparallelität bereit, sodass Sie denselben Vorgang für mehrere Datenelemente gleichzeitig ausführen können. Dies ist besonders nützlich, wenn Sie über große Datasets verfügen und Berechnungen oder Transformationen für jedes Element unabhängig voneinander durchführen möchten.
- Aufgabenbasierte asynchrone Programmierung: Die TPL stellt die
TaskKlasse bereit, die einen asynchronen Vorgang darstellt. Sie können dieasyncSchlüsselwörter verwenden,awaitum den Prozess des Schreibens asynchronen Codes zu vereinfachen. Auf diese Weise können Sie Code schreiben, der einfacher zu lesen und zu verwalten ist, während Sie gleichzeitig Parallelität nutzen. - Dataflow: Die TPL stellt ein Datenflussprogrammiermodell bereit, mit dem Sie komplexe Datenverarbeitungspipelines erstellen können. Dieses Modell basiert auf dem Konzept von "Blöcken", die Daten asynchron verarbeiten und mit Nachrichten miteinander kommunizieren können.
Von Bedeutung
Parallele Programmierung und Multithreading sind erweiterte Themen, die ein gutes Verständnis der Parallelität und Synchronisierung erfordern. Obwohl die TPL Multithread-Szenarien vereinfacht, empfehlen wir Ihnen, ein grundlegendes Verständnis von Threading-Konzepten zu haben, z. B. Sperren, Deadlocks und Race-Bedingungen, damit Sie die TPL effektiv verwenden können. Diese Schulung bietet eine begrenzte Einführung in parallele Programmierung mit der TPL.
Datenparallelität
Datenparallelität ist eine Form der parallelen Programmierung, die sich auf die gleichzeitige Ausführung desselben Vorgangs für mehrere Datenelemente konzentriert. Dies ist besonders nützlich, wenn Sie über große Datasets verfügen und Berechnungen oder Transformationen für jedes Element unabhängig voneinander durchführen möchten. In C# können Sie die Parallel.For Datenparallelität und Parallel.ForEach -methoden problemlos nutzen. Mit diesen Methoden können Sie über Sammlungen oder Datenbereiche parallel durchlaufen und die Arbeitsauslastung über mehrere Threads verteilen.
Die Aufgaben parallele Bibliothek unterstützt Datenparallelität über die System.Threading.Tasks.Parallel Klasse. Diese Klasse stellt methodenbasierte parallele Implementierungen von for und foreach Schleifen bereit. Sie schreiben die Schleifenlogik für eine Parallel.For- oder Parallel.ForEach-Schleife so, wie Sie eine sequenzielle Schleife schreiben würden. Die TPL verarbeitet alle Arbeit auf niedriger Ebene für Sie.
Das folgende Codebeispiel zeigt eine einfache Foreachschleife und die parallele Entsprechung.
// Sequential version
foreach (var item in sourceCollection)
{
Process(item);
}
// Parallel equivalent
Parallel.ForEach(sourceCollection, item => Process(item));
Die TPL bietet auch eine Reihe von Datenstrukturen, die für den gleichzeitigen Zugriff optimiert sind, z ConcurrentBag. B. , ConcurrentQueueund ConcurrentDictionary. Mit diesen Datenstrukturen können Sie Elemente sicher hinzufügen, entfernen und von mehreren Threads zugreifen, ohne dass explizit gesperrt werden muss.
Im folgenden Codebeispiel wird veranschaulicht, wie ConcurrentBag Ergebnisse aus mehreren parallel ausgeführten Aufgaben gespeichert werden:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Program
{
static void Main()
{
var results = new ConcurrentBag<int>();
Parallel.For(0, 100, i =>
{
// Simulate some work
Task.Delay(100).Wait();
results.Add(i);
});
Console.WriteLine($"Processed {results.Count} items in parallel.");
}
}
In diesem Beispiel wird verwendet ConcurrentBag , um die Ergebnisse der parallelen Verarbeitung zu speichern. Jeder Vorgang fügt dem Beutel sein Ergebnis hinzu, ohne explizite Sperren zu benötigen, um die Threadsicherheit zu gewährleisten.
Paralleles Ausführen und Task.WhenAll Ausführen Task.WhenAny von Aufgaben
Die Methoden und Task.WhenAll Methoden Task.WhenAny sind Teil der Task Parallel Library in C#. Mit diesen Methoden können Sie mehrere Aufgaben parallel ausführen und auf den Abschluss warten.
Task.WhenAll wird verwendet, wenn Sie warten möchten, bis alle Aufgaben abgeschlossen sind, bevor Sie fortfahren. Es akzeptiert ein Array von Aufgaben als Eingabe und gibt einen einzelnen Vorgang zurück, der den Abschluss aller Eingabeaufgaben darstellt. Dies ist nützlich, wenn Sie mehrere unabhängige Aufgaben haben, die gleichzeitig ausgeführt werden können, z. B. mehrere API-Aufrufe ausführen oder mehrere Dateien gleichzeitig verarbeiten.
Task.WhenAny wird verwendet, wenn Sie warten möchten, bis eine der Aufgaben abgeschlossen ist. Es verwendet ein Array von Aufgaben als Eingabe und gibt einen Vorgang zurück, der den ersten abgeschlossenen Vorgang darstellt. Dies ist nützlich, wenn Sie eine Aktion ausführen möchten, sobald eine der Vorgänge abgeschlossen ist, ohne darauf zu warten, dass alle abgeschlossen sind.
Im folgenden Codebeispiel wird veranschaulicht, wie Task.WhenAll Sie mehrere Aufgaben parallel ausführen und auf den Abschluss warten:
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Collections.Generic;
class Program
{
static async Task Main(string[] args)
{
var urls = new List<string>
{
"https://example.com",
"https://example.org",
"https://example.net"
};
var tasks = new List<Task<string>>();
foreach (var url in urls)
{
tasks.Add(FetchDataAsync(url));
}
// Wait for all tasks to complete
var results = await Task.WhenAll(tasks);
foreach (var result in results)
{
Console.WriteLine(result);
}
}
static async Task<string> FetchDataAsync(string url)
{
using (var client = new HttpClient())
{
return await client.GetStringAsync(url);
}
}
}
In diesem Beispiel ruft die FetchDataAsync Methode Daten aus mehreren URLs parallel mithilfe von Task.WhenAll. Die Ergebnisse werden nach Abschluss aller Aufgaben in die Konsole gedruckt.
Gleichzeitiges Ausführen mehrerer Datei-E/A-Vorgänge
In vielen Fällen ist die Dateiiteration ein Vorgang, der leicht parallelisiert werden kann.
Im folgenden Beispiel werden die Verzeichnisse sequenziell durchlaufen, die Dateien werden jedoch parallel verarbeitet. Dies ist wahrscheinlich der beste Ansatz, wenn Sie über ein großes Datei-zu-Verzeichnis-Verhältnis verfügen. Es ist auch möglich, die Verzeichnisiteration zu parallelisieren und auf jede Datei sequenziell zuzugreifen. Es ist wahrscheinlich nicht effizient, beide Schleifen zu parallelisieren, es sei denn, Sie sind speziell auf einen Computer mit einer großen Anzahl von Prozessoren ausgerichtet. Wie in allen Fällen sollten Sie Ihre Anwendung jedoch gründlich testen, um den besten Ansatz zu ermitteln.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
try
{
TraverseTreeParallelForEach(@"C:\Program Files", (f) =>
{
// Exceptions are no-ops.
try
{
// Do nothing with the data except read it.
byte[] data = File.ReadAllBytes(f);
}
catch (FileNotFoundException) { }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
catch (SecurityException) { }
// Display the filename.
Console.WriteLine(f);
});
}
catch (ArgumentException)
{
Console.WriteLine(@"The directory 'C:\Program Files' does not exist.");
}
// Keep the console window open.
Console.ReadKey();
}
public static void TraverseTreeParallelForEach(string root, Action<string> action)
{
//Count of files traversed and timer for diagnostic output
int fileCount = 0;
var sw = Stopwatch.StartNew();
// Determine whether to parallelize file processing on each folder based on processor count.
int procCount = Environment.ProcessorCount;
// Data structure to hold names of subfolders to be examined for files.
Stack<string> dirs = new Stack<string>();
if (!Directory.Exists(root))
{
throw new ArgumentException(
"The given root directory doesn't exist.", nameof(root));
}
dirs.Push(root);
while (dirs.Count > 0)
{
string currentDir = dirs.Pop();
string[] subDirs = { };
string[] files = { };
try
{
subDirs = Directory.GetDirectories(currentDir);
}
// Thrown if we do not have discovery permission on the directory.
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
// Thrown if another process has deleted the directory after we retrieved its name.
catch (DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
try
{
files = Directory.GetFiles(currentDir);
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (IOException e)
{
Console.WriteLine(e.Message);
continue;
}
// Execute in parallel if there are enough files in the directory.
// Otherwise, execute sequentially.Files are opened and processed
// synchronously but this could be modified to perform async I/O.
try
{
if (files.Length < procCount)
{
foreach (var file in files)
{
action(file);
fileCount++;
}
}
else
{
Parallel.ForEach(files, () => 0,
(file, loopState, localCount) =>
{
action(file);
return (int)++localCount;
},
(c) =>
{
Interlocked.Add(ref fileCount, c);
});
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
// Here we just output a message and go on.
Console.WriteLine(ex.Message);
return true;
}
// Handle other exceptions here if necessary...
return false;
});
}
// Push the subdirectories onto the stack for traversal.
// This could also be done before handing the files.
foreach (string str in subDirs)
dirs.Push(str);
}
// For diagnostic purposes.
Console.WriteLine($"Processed {fileCount} files in {sw.ElapsedMilliseconds} milliseconds");
}
}
In diesem Beispiel wird die Datei-E/A synchron ausgeführt. Wenn Ihr Code mit großen Dateien oder langsamen Netzwerkverbindungen arbeitet, ist es möglicherweise vorzuziehen, asynchron auf die Dateien zuzugreifen. Sie können asynchrone E/A-Techniken mit paralleler Iteration kombinieren.
Im Beispiel wird die lokale FileCount-Variable verwendet, um die Gesamtanzahl der verarbeiteten Dateien beizubehalten. Da auf die Variable gleichzeitig durch mehrere Aufgaben zugegriffen werden kann, wird der Zugriff durch Aufrufen der Interlocked.Add Methode synchronisiert.
Beachten Sie, dass die Threads, die von der ForEach-Methode gestartet werden, möglicherweise weiterhin ausgeführt werden, wenn eine Ausnahme im Hauptthread ausgelöst wird. Um diese Threads zu beenden, können Sie eine boolesche Variable in Den Ausnahmehandlern festlegen und den Wert für jede Iteration der parallelen Schleife überprüfen. Wenn der Wert angibt, dass eine Ausnahme ausgelöst wurde, verwenden Sie die ParallelLoopState-Variable, um die Schleife zu beenden oder zu unterbrechen.
Zusammenfassung
Diese Einheit konzentrierte sich auf Parallelität und Task Parallel Library (TPL). Es wird erläutert, wie asynchrone Aufgaben parallel mithilfe der Task.WhenAll Methode, der Datenparallelität und Parallel.ForParallel.ForEach Methoden und der Verwendung von gleichzeitigen Datenstrukturen wie ConcurrentBag, ConcurrentQueueund ConcurrentDictionary. Der Inhalt veranschaulicht außerdem, wie mehrere Datei-E/A-Vorgänge gleichzeitig ausgeführt werden.
Wichtige Punkte
- Die parallele Programmierung in C# ermöglicht das gleichzeitige Ausführen mehrerer Aufgaben.
- Die
TaskKlasseasyncundawaitSchlüsselwörter werden für die Implementierung der parallelen Programmierung verwendet. -
Task.WhenAllDie Methode wird verwendet, um zu warten, bis mehrere Aufgaben abgeschlossen sind, bevor Sie fortfahren. - Datenparallelität wird mithilfe
Parallel.ForundParallel.ForEachMethoden erreicht. - Gleichzeitige Datenstrukturen wie
ConcurrentBag,ConcurrentQueueundConcurrentDictionarysind für den gleichzeitigen Zugriff optimiert. -
Task.WhenAllundTask.WhenAnyMethoden ermöglichen das Parallele Ausführen mehrerer Aufgaben und warten auf den Abschluss. - Mehrere Datei-E/A-Vorgänge können gleichzeitig ausgeführt werden.