Exécuter des tâches asynchrones en parallèle
La programmation parallèle est une technique puissante qui vous permet d’exécuter plusieurs tâches simultanément, améliorant les performances et la réactivité de vos applications.
En C#, vous pouvez utiliser la bibliothèque parallèle de tâches (TPL) pour simplifier le processus d’écriture de code parallèle. Le TPL est un ensemble de types publics et d’API dans les espaces de noms System.Threading et System.Threading.Tasks. L’objectif du TPL est de rendre les développeurs plus productifs en simplifiant le processus d’ajout de parallélisme et d’accès concurrentiel aux applications. Le TPL ajuste dynamiquement le degré de concurrence pour utiliser tous les processeurs disponibles de la manière la plus efficace. En outre, le TPL gère le partitionnement du travail, la planification des threads sur le ThreadPool, la prise en charge de l’annulation, la gestion de l’état et d’autres détails de bas niveau. En utilisant TPL, vous pouvez optimiser les performances de votre code tout en vous concentrant sur le travail que votre programme est conçu pour accomplir.
Le TPL fournit une prise en charge dans les domaines suivants :
- Parallélisme des données : le TPL fournit des méthodes pour effectuer le parallélisme des données, ce qui vous permet d’effectuer la même opération sur plusieurs éléments de données simultanément. Cela est particulièrement utile lorsque vous avez des jeux de données volumineux et que vous souhaitez effectuer des calculs ou des transformations sur chaque élément indépendamment.
- Programmation asynchrone basée sur des tâches : le TPL fournit la
Taskclasse, qui représente une opération asynchrone. Vous pouvez utiliser les mots clésasyncetawaitpour simplifier le processus d’écriture de code asynchrone. Cela vous permet d’écrire du code plus facile à lire et à gérer tout en tirant parti du parallélisme. - Dataflow : le TPL fournit un modèle de programmation de flux de données qui vous permet de créer des pipelines de traitement de données complexes. Ce modèle est basé sur le concept de « blocs » qui peut traiter les données de manière asynchrone et communiquer entre eux à l’aide de messages.
Important
La programmation parallèle et la multithreading sont des rubriques avancées qui nécessitent une bonne compréhension de la concurrence et de la synchronisation. Même si la bibliothèque parallèle de tâches simplifie les scénarios multithread, il est recommandé de connaître les notions fondamentales des concepts de threading, tels que les verrous, les interblocages et les conditions de concurrence critique, afin de pouvoir utiliser efficacement la bibliothèque parallèle de tâches. Cette formation fournit une introduction limitée à la programmation parallèle à l’aide de la TPL.
Parallélisme des données
Le parallélisme des données est une forme de programmation parallèle qui se concentre sur l’exécution simultanée de la même opération sur plusieurs éléments de données. Cela est particulièrement utile lorsque vous avez des jeux de données volumineux et que vous souhaitez effectuer des calculs ou des transformations sur chaque élément indépendamment. En langage C#, vous pouvez utiliser les méthodes Parallel.For et Parallel.ForEach pour obtenir facilement le parallélisme des données. Ces méthodes vous permettent d’itérer sur des collections ou des plages de données en parallèle, en distribuant la charge de travail sur plusieurs threads.
La bibliothèque parallèle de tâches prend en charge le parallélisme des données par le biais de la System.Threading.Tasks.Parallel classe. Cette classe fournit des implémentations parallèles basées sur des méthodes pour les boucles for et foreach. Vous écrivez la logique de boucle pour une boucle Parallel.For ou une boucle Parallel.ForEach comme vous écririez une boucle séquentielle. Le TPL gère tout le travail de bas niveau pour vous.
L’exemple de code suivant montre une boucle foreach simple et son équivalent parallèle.
// Sequential version
foreach (var item in sourceCollection)
{
Process(item);
}
// Parallel equivalent
Parallel.ForEach(sourceCollection, item => Process(item));
Le TPL fournit également un ensemble de structures de données optimisées pour l’accès simultané, tels que ConcurrentBag, ConcurrentQueueet ConcurrentDictionary. Ces structures de données vous permettent d’ajouter, de supprimer et d’accéder en toute sécurité à des éléments de plusieurs threads sans avoir besoin de verrouillage explicite.
L’exemple de code suivant montre comment utiliser un ConcurrentBag pour stocker les résultats de plusieurs tâches exécutées en parallèle :
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.");
}
}
Dans cet exemple, il ConcurrentBag est utilisé pour stocker les résultats du traitement parallèle. Chaque tâche ajoute son résultat au sac sans avoir besoin de verrous explicites, garantissant ainsi la sécurité des threads.
Utiliser Task.WhenAll et Task.WhenAny exécuter des tâches en parallèle
Les méthodes Task.WhenAll et Task.WhenAny font partie de la bibliothèque parallèle de tâches (TPL) en C#. Ces méthodes vous permettent d’exécuter plusieurs tâches en parallèle et d’attendre leur achèvement.
Task.WhenAll est utilisé lorsque vous souhaitez attendre que toutes les tâches se terminent avant de continuer. Il prend un tableau de tâches comme entrée et retourne une tâche unique qui représente l’achèvement de toutes les tâches d’entrée. Cela est utile lorsque vous avez plusieurs tâches indépendantes qui peuvent être exécutées simultanément, telles que l’exécution simultanée de plusieurs appels d’API ou le traitement de plusieurs fichiers en même temps.
Task.WhenAny est utilisé lorsque vous souhaitez attendre la fin de l’une des tâches. Il prend un tableau de tâches comme entrée et retourne une tâche qui représente la première tâche qui se termine. Cela est utile lorsque vous souhaitez effectuer une action dès que l’une des tâches se termine, sans attendre que toutes les tâches se terminent.
L’exemple de code suivant montre comment exécuter Task.WhenAll plusieurs tâches en parallèle et attendre leur achèvement :
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);
}
}
}
Dans cet exemple, la méthode FetchDataAsync extrait des données de plusieurs URL en parallèle à l’aide de Task.WhenAll. Les résultats sont imprimés dans la console une fois toutes les tâches terminées.
Exécution simultanée de plusieurs opérations d’E/S de fichier
Dans de nombreux cas, l’itération de fichier est une opération qui peut être facilement parallélisée.
L’exemple suivant itère les répertoires de manière séquentielle, mais traite les fichiers en parallèle. Il s’agit probablement de la meilleure approche lorsque vous avez un ratio de fichiers à répertoires volumineux. Il est également possible de paralléliser l’itération du répertoire et d’accéder séquentiellement à chaque fichier. Il n’est probablement pas efficace de paralléliser les deux boucles, sauf si vous ciblez spécifiquement une machine avec un grand nombre de processeurs. Toutefois, comme dans tous les cas, vous devez tester soigneusement votre application pour déterminer la meilleure approche.
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");
}
}
Dans cet exemple, les E/S de fichier sont effectuées de manière synchrone. Lorsque votre code fonctionne avec des fichiers volumineux ou des connexions réseau lentes, il peut être préférable d’accéder aux fichiers de manière asynchrone. Vous pouvez combiner des techniques d’E/S asynchrones avec une itération parallèle.
L’exemple utilise la variable fileCount locale pour conserver le nombre total de fichiers traités. Étant donné que la variable peut être accessible simultanément par plusieurs tâches, l’accès est synchronisé en appelant la Interlocked.Add méthode.
Notez que si une exception est levée sur le thread principal, les threads démarrés par la méthode ForEach peuvent continuer à s’exécuter. Pour arrêter ces threads, vous pouvez définir une variable booléenne dans vos gestionnaires d’exceptions et vérifier sa valeur sur chaque itération de la boucle parallèle. Si la valeur indique qu’une exception a été levée, utilisez la variable ParallelLoopState pour arrêter ou interrompre la boucle.
Résumé
Cette unité s’est concentrée sur le parallélisme et la bibliothèque parallèle de tâches (TPL). Il explique comment exécuter des tâches asynchrones en parallèle à l’aide de la méthode Task.WhenAll, le parallélisme des données avec les méthodes Parallel.For et Parallel.ForEach, et l’utilisation de structures de données simultanées telles que ConcurrentBag, ConcurrentQueue et ConcurrentDictionary. Le contenu montre également comment effectuer plusieurs opérations d’E/S de fichier simultanément.
Points clés
- La programmation parallèle en C# permet d’exécuter plusieurs tâches simultanément.
- La classe
Task, les mots-clésasyncetawaitsont utilisés pour implémenter la programmation parallèle. -
Task.WhenAllla méthode est utilisée pour attendre la fin de plusieurs tâches avant de continuer. - Le parallélisme des données est obtenu à l’aide de
Parallel.ForetParallel.ForEachméthodes. - Les structures de données simultanées telles que
ConcurrentBag,ConcurrentQueueetConcurrentDictionarysont optimisées pour l’accès simultané. -
Task.WhenAlletTask.WhenAnyles méthodes permettent d’exécuter plusieurs tâches en parallèle et d’attendre leur achèvement. - Plusieurs opérations d’E/S de fichier peuvent être effectuées simultanément.