Partager via


ParallelHelper

ParallelHelper contient des API haute performance pour l’utilisation du code parallèle. Il contient des méthodes orientées performances, qui peuvent être utilisées pour configurer et exécuter rapidement des opérations parallèles sur un jeu de données, une plage d’itérations ou une zone précise.

API de plateforme : ParallelHelper, IAction, IAction2D, IRefAction<T>, IInAction<T><T>

Fonctionnement

Le type ParallelHelper est conçu autour de trois concepts principaux :

  • Il effectue un traitement par lot automatique sur la plage d’itérations cible. Cela signifie qu’il planifie automatiquement le bon nombre d’unités de travail en fonction du nombre de cœurs de processeur disponibles. Cela permet de réduire la surcharge liée à l’appel du rappel parallèle pour chaque itération parallèle.
  • Il tire profit de manière importante de la façon dont les types génériques sont implémentés en C#, et utilise des types struct implémentant des interfaces spécifiques à la place de délégués tels que Action<T>. Ainsi, le compilateur JIT peut « voir » chaque type de rappel utilisé, ce qui lui permet d’incorporer entièrement le rappel sous forme de code inline, quand cela est possible. Cette approche peut réduire considérablement la surcharge de chaque itération parallèle, en particulier quand de très petits rappels sont utilisés, dans la mesure où leur coût est négligeable par rapport à l’appel du délégué uniquement. De plus, l’utilisation d’un type struct en tant que rappel oblige les développeurs à gérer manuellement les variables capturées dans la fermeture, ce qui permet d’éviter les captures accidentelles du pointeur this à partir de méthodes d’instance et d’autres valeurs pouvant ralentir considérablement chaque appel de rappel. Il s’agit de la même approche que celle utilisée dans d’autres bibliothèques axées sur les performances, par exemple ImageSharp.
  • Il expose 4 types d’API qui représentent 4 types d’itérations différents : les boucles 1D et 2D, l’itération d’éléments avec effet secondaire et l’itération d’éléments sans effet secondaire. Chaque type d’action a un type interface correspondant qui doit être appliqué aux rappels struct passés aux API ParallelHelper : il s’agit de IAction, IAction2D, IRefAction<T> et IInAction<T><T>. Cela aide les développeurs à écrire du code dont l’intention est plus claire, et permet aux API d’effectuer des optimisations supplémentaires de manière interne.

Syntaxe

Prenons le scénario suivant : nous souhaitons traiter tous les éléments d’un tableau float[], et nous multiplions chacun d’eux par 2. Dans ce cas, nous n’avons pas besoin de capturer des variables : nous pouvons simplement utiliser l’interface IRefAction<T>, et ParallelHelper va charger automatiquement chaque élément à alimenter dans notre rappel. Il suffit de définir notre rappel, qui reçoit un argument ref float, et effectue l’opération nécessaire :

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Helpers;

// First declare the struct callback
public readonly struct ByTwoMultiplier : IRefAction<float>
{
    public void Invoke(ref float x) => x *= 2;
}

// Create an array and run the callback
float[] array = new float[10000];

ParallelHelper.ForEach<float, ByTwoMultiplier>(array);

Avec l’API ForEach, nous n’avons pas besoin de spécifier les plages d’itérations : ParallelHelper regroupe la collection par lot, et traite automatiquement chaque élément d’entrée. De plus, dans cet exemple spécifique, nous n’avons même pas eu à passer notre struct en tant qu’argument dans la mesure où il ne contenait aucun champ à initialiser. Il nous a suffi de spécifier son type en tant qu’argument de type au moment de l’appel de ParallelHelper.ForEach : cette API crée ensuite automatiquement une instance de struct, et l’utilise pour traiter les différents éléments.

Pour introduire le concept de fermetures, partons du principe que nous souhaitons multiplier les éléments de tableau par une valeur spécifiée au moment de l’exécution. Pour ce faire, nous devons « capturer » cette valeur dans notre type de rappel struct. Nous pouvons le faire de la façon suivante :

public readonly struct ItemsMultiplier : IRefAction<float>
{
    private readonly float factor;
    
    public ItemsMultiplier(float factor)
    {
        this.factor = factor;
    }

    public void Invoke(ref float x) => x *= this.factor;
}

// ...

ParallelHelper.ForEach(array, new ItemsMultiplier(3.14f));

Nous pouvons voir que struct contient désormais un champ qui représente le facteur à utiliser pour multiplier des éléments, au lieu d’utiliser une constante. Quand nous appelons ForEach, nous créons explicitement une instance de notre type de rappel, avec le facteur qui nous intéresse. De plus, dans ce cas, le compilateur C# sait également reconnaître automatiquement les arguments de type que nous utilisons, ce qui nous permet de les omettre ensemble dans l’appel de la méthode.

Cette approche de la création de champs pour les valeurs auxquelles nous devons accéder à partir d’un rappel nous permet de déclarer explicitement les valeurs à capturer, ce qui rend le code plus clair. Le compilateur C# effectue exactement la même chose en arrière-plan quand nous déclarons une fonction lambda ou une fonction locale, qui accède également à une variable locale.

Voici un autre exemple, qui utilise cette fois l’API For pour initialiser tous les éléments d’un tableau en parallèle. Notez que cette fois, nous capturons directement le tableau cible et que nous utilisons l’interface IAction pour notre rappel, ce qui donne à notre méthode l’index d’itération parallèle actif en tant qu’argument :

public readonly struct ArrayInitializer : IAction
{
    private readonly int[] array;

    public ArrayInitializer(int[] array)
    {
        this.array = array;
    }

    public void Invoke(int i)
    {
        this.array[i] = i;
    }
}

// ...

ParallelHelper.For(0, array.Length, new ArrayInitializer(array));

Remarque

Dans la mesure où les types de rappels sont des struct, ils sont passés par copie à chaque thread s’exécutant en parallèle, et non par référence. Cela signifie que les types valeur stockés en tant que champs dans les types de rappels sont également copiés. Pour ne pas oublier ce détail et éviter les erreurs, suivez la bonne pratique qui consiste à marquer le rappel struct en tant que readonly, pour que le compilateur C# ne nous laisse pas modifier les valeurs de ses champs. Cela s’applique uniquement aux champs d’instance d’un type valeur : si un rappel struct a un champ static de n’importe quel type, ou un champ de référence, cette valeur est correctement partagée entre les threads parallèles.

Méthodes

Il s’agit des 4 API principales exposées par ParallelHelper, et qui correspondent aux interfaces IAction, IAction2D, IRefAction<T> et IInAction<T>. Le type ParallelHelper expose également un certain nombre de surcharges pour ces méthodes, qui offrent plusieurs façons de spécifier la ou les plages d’itérations, ou le type du rappel d’entrée. For et For2D tirent parti des instances de IAction et IAction2D. Ils sont destinés à être utilisés quand un travail parallèle doit être effectué, et qu’il n’est pas nécessairement mappé à une collection sous-jacente accessible directement avec les index de chaque itération parallèle. En revanche, les surcharges de ForEach tirent parti des instances de IRefAction<T> et IInAction<T>, et peuvent être utilisées quand les itérations parallèles sont directement mappées aux éléments d’une collection pouvant être indexée directement. Dans ce cas, elles font également abstraction de la logique d’indexation. Ainsi, chaque appel parallèle se concentre uniquement sur l’élément d’entrée à utiliser, et non sur la façon de récupérer cet élément.

Exemples

Vous trouverez d’autres exemples dans les tests unitaires.