Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
MSBuild 18.6 introduit la fonctionnalité de génération en parallèle dans le même processus. Pour activer ce mode, spécifiez l’option de ligne de commande -mt. Les versions précédentes de MSBuild ont pris en charge les builds parallèles, mais les builds ont été effectuées dans des processus distincts. Cette modification a un impact sur la façon dont vous créez des tâches. Alors qu’auparavant, les tâches s’exécuteraient dans un processus distinct, désormais toutes les tâches multithread s’exécutent dans le même processus. Bien que la plupart de la logique n’ait pas besoin de changer, il existe des constructions au niveau du processus qui doivent être gérées plus attentivement. Les constructions au niveau du processus incluent le répertoire de travail actuel, les variables d’environnement et les informations de démarrage du processus (ProcessStartInfo).
Pour prendre en charge ces modifications, MSBuild 18.6 introduit l’interface IMultiThreadableTask (dans Microsoft.Build.Framework) et la classe TaskEnvironment.
TaskEnvironment comprend une propriété ProjectDirectory et des méthodes telles que GetAbsolutePath(), GetEnvironmentVariable(), SetEnvironmentVariable() et GetProcessStartInfo().
Important
Le mode multithread est actuellement disponible en tant que fonctionnalité expérimentale ; il n’est pas recommandé pour l’utilisation en production pour l’instant. La mise à jour de vos dépendances de bibliothèque MSBuild pour utiliser les API en mode multithread empêche implicitement vos bibliothèques de s’exécuter sur des versions antérieures de Visual Studio et MSBuild. Nous encourageons les utilisateurs précoces à essayer le mode multithread et à fournir des commentaires. Envoyez des problèmes au dépôt MSBuild GitHub.
L’interface IMultiThreadableTask définit le contrat pour les tâches susceptibles de s’exécuter dans le processus lors de builds multithreadés :
// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
TaskEnvironment TaskEnvironment { get; set; }
}
Pour migrer une tâche, implémentez IMultiThreadableTask en même temps que votre classe de base existante Task et exposez la TaskEnvironment propriété :
public class MyTask : Task, IMultiThreadableTask
{
// Initialize to Fallback so the task works safely outside the MSBuild engine (for example, in unit tests).
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
// ...
}
Les tâches qui implémentent IMultiThreadableTask peuvent s’exécuter en cours d’exécution. Toutes ces tâches doivent également porter l’attribut [MSBuildMultiThreadableTask] , qui est le marqueur que MSBuild utilise pour choisir la tâche en exécution in-process. Avant d’ajouter l’attribut, vérifiez que la tâche n’a pas de dépendances sur les constructions au niveau du processus, comme le répertoire de travail actuel ou l’environnement, et que son code est thread-safe. Veillez à ce que l’accès thread-safe aux variables statiques soit assuré, car ces variables sont partagées entre toutes les instances de tâche et peuvent être accessibles ou modifiées par différentes instances de la tâche qui s’exécutent également dans le même processus.
Exemple de tâche : BuildCommentTask
L’exemple AddBuildCommentTask suivant est utilisé dans cet article pour illustrer le processus de migration. Cette tâche ajoute un commentaire de build aux fichiers texte. Par défaut, il écrit du texte brut ; les propriétés facultatives CommentPrefix et CommentSuffix permettent aux appelants d’encapsuler le commentaire dans la syntaxe appropriée en langage (par exemple, // pour C#, <!-- et --> pour XML, # pour Python ou YAML) :
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using RequiredAttribute = Microsoft.Build.Framework.RequiredAttribute;
namespace BuildCommentTask
{
public class AddBuildCommentTask : Microsoft.Build.Utilities.Task
{
private static int ModifiedFileCount = 0;
// Callers are responsible for passing only text files in TargetFiles,
// and for setting CommentPrefix/CommentSuffix to match the file type.
[Required]
public ITaskItem[] TargetFiles { get; set; }
[Required]
public string VersionNumber { get; set; }
// Optional CommentPrefix and CommentSuffix wrap the comment in
// language-appropriate syntax, e.g., "// " for C# or "# " for Python.
// Include any desired spacing in the prefix or suffix value.
public string CommentPrefix { get; set; } = "";
public string CommentSuffix { get; set; } = "";
public override bool Execute()
{
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
if (!string.IsNullOrEmpty(disableComments))
{
Log.LogMessage(MessageImportance.Normal, "Build comments disabled via environment variable.");
return true;
}
string buildDate = DateTime.UtcNow.ToString("yyyy-MM-dd");
string commentPattern = $@"^{Regex.Escape(CommentPrefix)}\s*Build Date:.*Version:.*{Regex.Escape(CommentSuffix)}$";
foreach (var item in TargetFiles)
{
var filePath = item.ItemSpec;
try
{
string[] originalLines = File.ReadAllLines(filePath);
if (originalLines.Length > 0 && Regex.IsMatch(originalLines[0], commentPattern))
{
Log.LogMessage(MessageImportance.Low, $"Skipped (already annotated): {filePath}");
continue;
}
ModifiedFileCount++;
string comment = $"{CommentPrefix}Build Date: {buildDate}, Version: {VersionNumber}, File #: {ModifiedFileCount}{CommentSuffix}";
// Note: rewriting a file in place like this is convenient for a sample but is not
// recommended in production tasks. Prefer writing to a separate output file instead.
File.WriteAllLines(filePath, new[] { comment }.Concat(originalLines));
Log.LogMessage(MessageImportance.High, $"Added build comment to: {filePath}");
}
catch (Exception ex)
{
Log.LogError($"Failed to process {filePath}: {ex.Message}");
return false;
}
}
return true;
}
}
}
Un fichier projet peut appeler cette tâche pour différents types de fichiers, en passant la syntaxe de commentaire appropriée pour chacun d’eux :
<!-- Stamp generated text files with plain text (no comment prefix) -->
<AddBuildCommentTask
TargetFiles="@(GeneratedFiles)"
VersionNumber="$(Version)" />
<!-- Stamp C# source files with // comments -->
<AddBuildCommentTask
TargetFiles="@(Compile)"
VersionNumber="$(Version)"
CommentPrefix="// " />
<!-- Stamp XML content files with <!-- --> comments -->
<AddBuildCommentTask
TargetFiles="@(Content -> WithMetadataValue('Extension', '.xml'))"
VersionNumber="$(Version)"
CommentPrefix="<!-- "
CommentSuffix=" -->" />
Cette tâche comporte quatre problèmes de sécurité des threads qui doivent être résolus pour les builds multithread :
-
Chemins relatifs :
File.ReadAllLinesetFile.WriteAllLinesutiliseritem.ItemSpecdirectement, qui peut être un chemin relatif. En mode multithreadé, le répertoire de travail du processus n’est pas nécessairement le répertoire du projet. -
Champ statique :
ModifiedFileCountest unstaticchamp partagé entre toutes les instances, ce qui provoque des courses de données lorsque plusieurs builds s’exécutent simultanément. -
Variables d’environnement : le problème de variable d’environnement le plus courant dans les builds multithreads est des tâches qui définissent des variables d’environnement avant de générer un processus enfant, en attendant que l’enfant hérite de ces variables. En mode multithreadé,
Environment.SetEnvironmentVariable()modifie l’environnement au niveau du processus, partagé par toutes les compilations concurrentes, de sorte qu’une modification destinée au processus enfant d’un projet peut déborder sur celui d’un autre projet. La lecture des variables d’environnement directement dans le code de tâche (Environment.GetEnvironmentVariable()) est généralement une mauvaise pratique ; Les propriétés MSBuild sont une meilleure alternative, car elles sont journalisées et traceables.
Important
Le mode de compilation multithread est actuellement disponible uniquement pour les compilations en ligne de commande (dotnet build et MSBuild.exe). Les compilations MSBuild de Visual Studio ne prennent pas encore en charge l’exécution multithreadée dans le processus. Dans Visual Studio, toute l’exécution des tâches continue à s’exécuter hors du processus. L’intégration à Visual Studio est prévue dans une prochaine version.
Prerequisites
MSBuild 18.6 ou version ultérieure.
Activez l’exécution multithreadée des tâches à l’aide du commutateur de ligne de commande
-mt:dotnet build -mtPour plus d’informations sur le
-mtcommutateur, consultez la référence de ligne de commande MSBuild.
Planifier la migration
Passez en revue votre code de tâche pour connaître les problèmes suivants :
- Vérifiez le code de tâche et identifiez toute utilisation des chemins relatifs. Vérifiez toutes les E/S d’entrée et de fichier.
- Recherchez les utilisations de variables d’environnement.
- Vérifiez toute
ProcessStartInfoutilisation de l’API. - Vérifiez les champs statiques ou les structures de données et utilisez des méthodes standard pour les rendre thread-safe.
- Si aucun des éléments ci-dessus ne s’applique, envisagez d’ajouter l’attribut uniquement.
- Tenez compte des exigences particulières pour prendre en charge les versions antérieures de MSBuild. Consultez la prise en charge des versions antérieures de MSBuild.
Guide de référence rapide pour le remplacement d’API
Le tableau suivant récapitule les API .NET que vous devez remplacer et leurs équivalents TaskEnvironment :
| API .NET à éviter | Niveau | Remplacement |
|---|---|---|
Path.GetFullPath(path) |
ERROR | Consultez la remarque qui suit ce tableau |
File.* avec des chemins relatifs |
ERROR | Résoudre d’abord avec TaskEnvironment.GetAbsolutePath() |
Directory.* avec des chemins relatifs |
ERROR | Résoudre d’abord avec TaskEnvironment.GetAbsolutePath() |
Environment.GetEnvironmentVariable() |
ERROR | TaskEnvironment.GetEnvironmentVariable() |
Environment.SetEnvironmentVariable() |
ERROR | TaskEnvironment.SetEnvironmentVariable() |
Environment.CurrentDirectory |
ERROR | TaskEnvironment.ProjectDirectory |
new ProcessStartInfo() |
ERROR | TaskEnvironment.GetProcessStartInfo() |
Process.Start() |
ERROR | Utiliser ToolTask ou TaskEnvironment.GetProcessStartInfo() |
| Champs statiques | AVERTISSEMENT | Utiliser des champs d’instance ou des collections sécurisées pour les threads |
Note
Path.GetFullPath(path) effectue deux opérations : elle convertit un chemin relatif en chemin absolu et produit une forme canonique du chemin (résolution . et .. segments). Ces éléments doivent être gérés séparément :
-
Chemin absolu uniquement : Utiliser
TaskEnvironment.GetAbsolutePath(path). Cette approche est suffisante pour la plupart des opérations d'E/S de fichier où vous transmettez directement le chemin d'accès aux API .NET. -
Chemin canonique : si vous vous appuyez sur le formulaire canonique (par exemple, lors de l’utilisation d’un chemin d’accès en tant que clé de cache ou de dictionnaire), utilisez
Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path))pour obtenir un chemin absolu canonique entièrement résolu.
Marquer la tâche avec l’attribut
Toutes les tâches qui prennent part à des générations multithreadées doivent être marquées à l’aide de l’attribut [MSBuildMultiThreadableTask]. Cet attribut est le signal que MSBuild utilise pour identifier les tâches qui sont sécurisées pour s’exécuter en cours d’exécution.
[MSBuildMultiThreadableTask]
public class MyTask : Task
{
public override bool Execute()
{
// Task logic that doesn't depend on process-level state
return true;
}
}
Si votre tâche est déjà thread-safe et n’utilise aucune API au niveau du processus (répertoire de travail actuel, variables d’environnement, ProcessStartInfo), l’attribut seul est tout ce dont vous avez besoin. La tâche continue d’hériter de Task (ou ToolTask) sans aucune autre modification.
Si votre tâche doit remplacer les appels d’API au niveau du processus (par exemple, pour résoudre les chemins relatifs ou lire les variables d’environnement en toute sécurité), implémentez IMultiThreadableTaskégalement . Cette interface donne à votre tâche accès à la propriété TaskEnvironment. L’attribut reste requis dans les deux cas ; IMultiThreadableTask est une étape supplémentaire qui déverrouille le TaskEnvironment API.
Note
MSBuild détecte le MSBuildMultiThreadableTaskAttribute uniquement d’après son espace de noms et son nom, sans tenir compte de l’assembly dans lequel il est défini. Cela signifie que vous pouvez définir l’attribut vous-même dans votre propre code (voir Prise en charge des versions antérieures de MSBuild) et MSBuild le reconnaît toujours.
Note
Il MSBuildMultiThreadableTaskAttribute n’est pas héritable (Inherited = false). Chaque classe de tâche doit déclarer explicitement l’attribut à reconnaître comme multithreadable. Hériter d’une classe qui a l’attribut ne rend pas automatiquement la classe dérivée multithreadable.
Initialiser TaskEnvironment en mode de secours
Lors de l’implémentation IMultiThreadableTask, initialisez la TaskEnvironment propriété sur TaskEnvironment.Fallback:
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
MSBuild définit cette propriété avant d’appeler Execute() dans une build normale. La Fallback valeur par défaut garantit que la tâche fonctionne correctement dans d’autres scénarios d’hébergement (tels que les tests unitaires ou les outils d’orchestration de build personnalisés) où MSBuild n’est pas présent pour définir la propriété. Sans cela, l’accès à TaskEnvironment en dehors du moteur lèverait une exception de référence null.
Si vous devez prendre en charge les versions MSBuild antérieures à la version 18.6 qui n’incluent TaskEnvironment.Fallbackpas, initialisez la propriété à null la place et protégez les TaskEnvironment appels avec une vérification null. Pour plus d’options, consultez Prise en charge des versions antérieures de MSBuild .
Mettre à jour les chemins d’accès et les entrées/sorties de fichiers
Une tâche accepte souvent des entrées, telles que des listes d’éléments dans MSBuild, qui, si elles sont des fichiers, peuvent être sous la forme de chemins relatifs.
Les chemins relatifs sont toujours relatifs au répertoire de travail actuel du processus, mais, étant donné que la tâche s’exécute maintenant dans le processus, le répertoire de travail peut ne pas être identique au moment où la tâche s’est exécutée dans son propre processus. Ces chemins sont relatifs au répertoire du projet. Le TaskEnvironment comprend une propriété ProjectDirectory et une méthode GetAbsolutePath() que vous pouvez utiliser pour convertir des chemins relatifs en chemins absolus. Vous pouvez également accéder à la métadonnée FullPath ; il n’est pas nécessaire d’utiliser le chemin relatif ItemSpec, puis de le convertir en chemin absolu.
Le type AbsolutePath
AbsolutePath est un struct en lecture seule dans Microsoft.Build.Framework qui représente un chemin d’accès absolu validé. Les membres clés sont les suivants :
public readonly struct AbsolutePath : IEquatable<AbsolutePath>
{
public string Value { get; }
public string OriginalValue { get; }
public AbsolutePath(string path); // Validates Path.IsPathRooted
public AbsolutePath(string path, AbsolutePath basePath);
public static implicit operator string(AbsolutePath path);
}
Le AbsolutePath constructeur vérifie que le chemin fourni est rooté. Vous pouvez également construire un AbsolutePath en fournissant un chemin d’accès relatif et un chemin d’accès de base. La conversion implicite en string signifie que vous pouvez passer un AbsolutePath directement à toute API qui attend un chemin d’accès string.
La propriété OriginalValue conserve la chaîne du chemin d’accès d’origine telle qu’elle a été fournie avant sa résolution. Cette propriété est utile lorsque vous devez conserver les chemins d’accès relatifs dans les sorties de tâche ou les messages de journalisation. Par exemple, une tâche qui journalise les fichiers qu’il a traités peut utiliser OriginalValue dans ses messages journaux afin que les chemins d’accès dans la sortie restent relatifs et lisibles, tout en utilisant la conversion résolue Value (ou la conversion implicite string ) pour les E/S de fichiers réels.
Utilisez TaskEnvironment.GetAbsolutePath() pour résoudre les chemins d’accès des éléments :
Avant :
var filePath = item.ItemSpec;
string[] originalLines = File.ReadAllLines(filePath);
// Note: rewriting a file in place like this is convenient for a sample but is not
// recommended in production tasks. Prefer writing to a separate output file instead.
File.WriteAllLines(filePath, new[] { comment }.Concat(originalLines));
Après :
AbsolutePath filePath = TaskEnvironment.GetAbsolutePath(item.ItemSpec);
string[] originalLines = File.ReadAllLines(filePath); // AbsolutePath converts to string implicitly
// Note: rewriting a file in place like this is convenient for a sample but is not
// recommended in production tasks. Prefer writing to a separate output file instead.
File.WriteAllLines(filePath, new[] { comment }.Concat(originalLines));
// Use filePath.OriginalValue in log messages to preserve the relative path as written by the user
Log.LogMessage(MessageImportance.High, $"Added build comment to: {filePath.OriginalValue}");
Gérer la contention de fichiers dans les builds parallèles
La contention de fichiers peut se produire chaque fois que plusieurs tâches s’exécutent en parallèle et accèdent au même fichier. Cette préoccupation s’applique aussi bien au modèle multiprocessus traditionnel qu’au mode multithread dans le processus plus récent. Dans les deux cas, le même fichier peut être accessible simultanément lorsque :
- Le même fichier apparaît dans plusieurs builds de sous-projet (par exemple, un fichier de configuration partagé ou un fichier source lié).
- Une tâche lit et écrit un fichier qu’une autre instance de tâche traite également.
Les méthodes pratiques comme File.ReadAllLines et File.WriteAllLines ne fournissent pas de contrôle explicite sur le verrouillage de fichiers. Lorsque l’accès concurrent est possible, utilisez FileStream avec un partage et un verrouillage explicites :
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
// FileShare.None ensures exclusive access; other attempts
// to open this file will throw IOException until the stream
// is disposed.
using var reader = new StreamReader(stream);
string content = reader.ReadToEnd();
stream.SetLength(0); // Truncate before rewriting.
stream.Position = 0;
using var writer = new StreamWriter(stream);
writer.WriteLine(comment);
writer.Write(content);
}
Principales recommandations concernant les entrées/sorties de fichiers dans les tâches multithread :
- Utiliser
FileShare.Nonepour les opérations de lecture-modification-écriture. Ce paramètre empêche une autre tâche de lire le contenu obsolète pendant la mise à jour du fichier. - Capturez
IOExceptionet envisagez de réessayer. Quand une autre tâche ou un autre processus détient un verrou, votre tentative d’ouverture génèreIOException. Une brève nouvelle tentative avec temporisation progressive est souvent appropriée. - Évitez de conserver des verrous sur plusieurs fichiers à la fois. Si deux tâches verrouillent chacun un fichier, puis essayez de verrouiller l’autre, vous obtenez un blocage. Si vous devez utiliser plusieurs fichiers, verrouillez-les dans un ordre cohérent (par exemple, triés par chemin d’accès complet).
- Conservez les verrous aussi courts que possible. Ouvrez le fichier, lisez, modifiez, écrivez et fermez-le en une seule opération. Ne gardez pas un verrou sur un fichier pendant que vous effectuez d’autres tâches sans rapport.
L’exemple précédent est une approche. Pour obtenir des conseils généraux sur les opérations d’E/S de fichiers sécurisées pour les threads dans .NET, consultez classe FileStream, énumération FileShare, et Meilleures pratiques pour le threading managé.
Note
TaskEnvironment lui-même n’est pas sécurisé pour les threads. Cela importe uniquement si votre tâche génère en interne ses propres threads (par exemple, à l’aide Parallel.ForEach ou Task.Run). La plupart des tâches ne le font pas. Ils implémentent Execute() de façon linéaire et permettent à MSBuild de gérer le parallélisme entre les instances de tâche. Si votre tâche crée ses propres threads, stockez dans des variables locales les valeurs de TaskEnvironment avant de les lancer, plutôt que d’accéder à TaskEnvironment simultanément depuis plusieurs threads.
Mettre à jour les variables d’environnement
Note
La lecture des variables d’environnement dans le code de tâche est généralement une mauvaise pratique, même dans les builds à thread unique. Les propriétés MSBuild sont une meilleure alternative : elles sont explicitement délimitées, enregistrées pendant la génération et traceables dans le journal de génération. Si votre tâche lit actuellement une variable d’environnement pour recevoir une entrée, envisagez de la remplacer par une propriété de tâche à la place. Le projet peut toujours dériver la valeur d’une variable d’environnement : <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.
Les instructions de cette section concernent la migration de tâches existantes qui s’appuient déjà sur des variables d’environnement. Si vous avez la possibilité de refactoriser, préférez les propriétés et les éléments.
Définition des variables d’environnement pour les processus enfants
Le problème de variable d’environnement le plus courant dans les builds multithreads est une tâche qui définit une variable d’environnement, puis génère un processus enfant, attendant que l’enfant hérite de celui-ci. Dans le modèle à plusieurs processus, Environment.SetEnvironmentVariable() a modifié en toute sécurité l’environnement du processus de travail pour ce projet. En mode multithreadé, le processus est partagé entre toutes les compilations concurrentes, de sorte qu’une modification destinée au processus enfant d’un projet peut se répercuter sur celui d’un autre projet.
Utiliser TaskEnvironment.SetEnvironmentVariable() conjointement avec TaskEnvironment.GetProcessStartInfo() (voir Mettre à jour les appels d’API ProcessStart).
GetProcessStartInfo() retourne un ProcessStartInfo prérempli avec le répertoire de travail du projet et sa table isolée des variables d’environnement, y compris toutes les variables que vous définissez avec SetEnvironmentVariable(), afin que les processus enfants héritent automatiquement du bon environnement, limité à la portée du projet.
Avant :
Environment.SetEnvironmentVariable("TOOL_OUTPUT_DIR", outputDir);
var startInfo = new ProcessStartInfo("mytool.exe") { UseShellExecute = false };
Process.Start(startInfo); // inherits the modified process-level environment
Après :
TaskEnvironment.SetEnvironmentVariable("TOOL_OUTPUT_DIR", outputDir);
ProcessStartInfo startInfo = TaskEnvironment.GetProcessStartInfo();
startInfo.FileName = "mytool.exe";
startInfo.UseShellExecute = false;
Process.Start(startInfo); // inherits the project-scoped environment
Lecture des variables d’environnement dans les tâches existantes
Si votre tâche existante lit les variables d’environnement et que vous ne pouvez pas refactoriser immédiatement les propriétés de tâche, remplacez Environment.GetEnvironmentVariable() par TaskEnvironment.GetEnvironmentVariable(). Cet appel de méthode lit à partir de la table d’environnement délimitée par le projet plutôt que dans l’environnement de processus partagé, de sorte que les builds simultanées n’interfèrent pas entre elles.
Avant (de BuildCommentTask) :
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Après :
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Conseil / Astuce
Lors de la mise à jour du code existant qui lit une variable d’environnement, envisagez de remplacer le modèle par une propriété de tâche. Par exemple, exposez public bool DisableComments { get; set; } sur la tâche et laissez le projet transmettre DisableComments="$(DISABLE_BUILD_COMMENTS)". MSBuild journalise la valeur résolue, ce qui le rend visible dans le journal de build et est beaucoup plus facile à diagnostiquer qu’une variable d’environnement masquée lue.
Mettre à jour les appels d’API ProcessStart
En règle générale, si une tâche démarre un processus, vous devez utiliser ToolTask, qui gère tout pour vous. Dans les cas où vous mettez à jour une tâche qui appelle ProcessStartInfo directement, utilisez TaskEnvironment.GetProcessStartInfo(). Cela renvoie un ProcessStartInfo configuré avec le répertoire de travail du projet et sa table d’environnement isolé. Si vous définissez également des variables d’environnement avant le lancement, utilisez TaskEnvironment.SetEnvironmentVariable() d’abord, comme indiqué dans la section précédente.
Avant :
var startInfo = new ProcessStartInfo("mytool.exe")
{
WorkingDirectory = ".",
UseShellExecute = false
};
Process.Start(startInfo);
Après :
ProcessStartInfo startInfo = TaskEnvironment.GetProcessStartInfo();
startInfo.FileName = "mytool.exe";
startInfo.UseShellExecute = false;
Process.Start(startInfo);
Note
Si votre tâche hérite de ToolTask, les informations de démarrage du processus sont déjà prises en charge pour vous. Vous devez uniquement mettre à jour les tâches qui créent ProcessStartInfo directement.
Mettre à jour les champs statiques et les structures de données pour qu’elles soient thread-safe
Les champs statiques nécessitent une attention particulière lorsque vous migrez vers des versions multithreadées. Même dans le modèle multiprocesseur, un seul processus peut générer plusieurs projets, de sorte que l’état statique est partagé, tout simplement pas simultanément.
Le mode multithread ajoute une nouvelle dimension à ce problème. Plusieurs builds peuvent désormais partager le même processus et exécuter des tâches simultanément (en particulier avec MSBuild Server, qui est automatiquement activée avec le multithreading). Un champ statique est partagé entre toutes les instances de tâche du processus, non seulement dans votre build, mais potentiellement entre les appels de build distincts s’exécutant simultanément. Par exemple, deux développeurs s’exécutant dotnet build en même temps sur un serveur de build, ou deux fenêtres de terminal sur le même ordinateur, peuvent partager le même état statique, et maintenant ces builds y accèdent en même temps.
Dans l’exemple BuildCommentTask , le champ ModifiedFileCount statique est partagé entre toutes les instances :
Avant :
private static int ModifiedFileCount = 0;
// In Execute():
ModifiedFileCount++;
Ce code présente deux problèmes. Tout d’abord, l’opérateur ++ n’est pas atomique. Lorsque plusieurs instances de tâche s’exécutent simultanément, deux threads peuvent lire la même valeur et écrire le même résultat incrémenté, ce qui entraîne la perte de nombres. Deuxièmement, étant donné que le champ est statique, il persiste entre les builds et est partagé entre les builds simultanées dans le même processus.
Les sections suivantes montrent deux approches pour résoudre ces problèmes, de la plus simple au plus correcte.
Approche 1 : Utiliser une API thread-safe, mais à l’échelle du processus
Le correctif le plus simple consiste à rendre l’incrément atomique :
private static int ModifiedFileCount = 0;
// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);
Interlocked.Increment effectue la séquence lecture-incrémentation-écriture comme une seule opération atomique, de sorte qu’aucune incrémentation n’est perdue. Cette approche résout le problème d’accès concurrentiel, mais le compteur reste partagé entre toutes les compilations au sein du processus, y compris les compilations consécutives et les compilations concurrentes. Si deux builds s’exécutent simultanément, leurs numéros de fichiers s’intercalent (la build A obtient #1, #3, #5 ; la build B obtient #2, #4, #6). Le fait que cette situation soit acceptable dépend de la nécessité, pour votre tâche, d’une isolation pour chaque build. Pour un compteur de numérotation de fichiers séquentiel comme ModifiedFileCount, le partage entre builds est un problème de correction ; utilisez RegisterTaskObject plutôt (voir Approche 2).
Ici, l’équivalent d’API sûr vis-à-vis des threads, mais à l’échelle du processus, est InterlockedIncrement, mais dans votre code, vous devrez trouver des solutions de remplacement appropriées et sûres vis-à-vis des threads pour toutes les API qui ne le sont pas. Par exemple, si votre tâche conserve l’état à l’aide d’un Dictionary, pensez à utiliser ConcurrentDictionary<TKey,TValue>.
Approche 2 : RegisterTaskObject pour l’isolation délimitée par la build
Si votre tâche a besoin d’un état statique partagé entre les sous-projets dans une seule exécution de build, mais isolé des autres builds concurrents, utilisez IBuildEngine4.RegisterTaskObject avec RegisteredTaskObjectLifetime.Build. MSBuild gère la durée de vie de l’objet, qui est créé lors de la première utilisation et nettoyé lors de la fin de la build. Notez que les objets inscrits doivent être thread-safe.
Tout d’abord, définissez une classe de compteur thread-safe simple :
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
Utilisez ensuite une méthode utilitaire avec un verrouillage à double vérification pour obtenir ou créer le compteur :
private static readonly object s_counterLock = new();
private FileCounter GetOrCreateCounter()
{
const string key = "BuildCommentTask.FileCounter";
var counter = BuildEngine4.GetRegisteredTaskObject(
key, RegisteredTaskObjectLifetime.Build) as FileCounter;
if (counter == null)
{
lock (s_counterLock)
{
counter = BuildEngine4.GetRegisteredTaskObject(
key, RegisteredTaskObjectLifetime.Build) as FileCounter;
if (counter == null)
{
counter = new FileCounter();
BuildEngine4.RegisterTaskObject(
key, counter,
RegisteredTaskObjectLifetime.Build,
allowEarlyCollection: false);
}
}
}
return counter;
}
Dans Execute() :
FileCounter counter = GetOrCreateCounter();
// ...
int fileNumber = counter.Next();
Avec cette approche, chaque invocation de compilation dispose de son propre FileCounter. Tous les sous-projets d’une même build partagent le compteur (numérotation séquentielle), mais un dotnet build distinct exécuté simultanément sur la même machine utilise un compteur différent.
RegisteredTaskObjectLifetime.Build indique à MSBuild de limiter l’objet à l’invocation de build en cours et de le supprimer lorsque la génération se termine.
Choisir l’approche appropriée
Lorsque vous décidez comment gérer l’état statique, commencez à partir de cette question : ces données sont-elles sécurisées pour partager toutes les builds qui peuvent jamais s’exécuter dans le même processus, y compris les builds consécutives et les builds simultanées ?
Les processus de travail MSBuild persistent entre les appels (la réutilisation des nœuds est activée par défaut) et un processus MSBuild peut potentiellement servir plusieurs builds de solution au cours de sa durée de vie, pas seulement dans un seul dotnet build appel. Ne supposez pas qu’un processus gère une seule build.
Utilisez ces instructions :
- Conservez le champ statique uniquement si les données mises en cache sont sécurisées pour accéder à partir de plusieurs threads entre différents projets et entre plusieurs builds sans nécessiter d’invalidation entre les builds. Par exemple, un cache de données immuables calculées une fois à partir d’entrées qui ne changent jamais (telles que les métadonnées d’assembly chargées une fois au démarrage) peut être éligible.
-
Utilisez-le
IBuildEngine4.RegisterTaskObjectRegisteredTaskObjectLifetime.Buildlorsque l’état doit être isolé par appel de build (par exemple, compteurs, accumulations ou caches qui doivent être réinitialisés entre les builds ou ne pas fuiter entre les builds simultanées). Il s’agit de l’approche recommandée pour la plupart des états mutables partagés. -
Utilisez les primitives
System.Threading(Interlocked,ConcurrentDictionary,lock,ReaderWriterLockSlim) pour rendre tout état statique conservé sûr vis-à-vis des threads, mais n’oubliez pas que la sûreté des threads à elle seule n’assure pas une isolation au niveau de la compilation. Consultez les meilleures pratiques en matière de threads managés.
Conseil / Astuce
L’exemple complet de migration présenté plus loin dans cet article utilise l’approche RegisterTaskObject pour illustrer l’isolation limitée à la build.
Exemple de migration complet
Le code suivant montre la migration AddBuildCommentTask complète avec les cinq modifications appliquées :
- Possède l’attribut
[MSBuildMultiThreadableTask], ce qui le marque pour une exécution dans le processus. - Implémente
IMultiThreadableTaskaux côtés de la classe de base existanteTasket expose la propriétéTaskEnvironment. - Utilise
TaskEnvironment.GetAbsolutePath()pour la résolution du chemin d’accès. - Utilise
TaskEnvironment.GetEnvironmentVariable()au lieu deEnvironment.GetEnvironmentVariable(). - Utilise
IBuildEngine4.RegisterTaskObjectavecRegisteredTaskObjectLifetime.Buildpour étendre le compteur de fichiers à l’appel de build actuel, en remplaçant le compteur statique à l’échelle du processus.
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
namespace BuildCommentTask
{
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
[MSBuildMultiThreadableTask]
public class AddBuildCommentTask : Task, IMultiThreadableTask
{
private static readonly object s_counterLock = new();
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
// Callers are responsible for passing only text files in TargetFiles,
// and for setting CommentPrefix/CommentSuffix to match the file type.
[Required]
public ITaskItem[] TargetFiles { get; set; }
[Required]
public string VersionNumber { get; set; }
// Optional CommentPrefix and CommentSuffix wrap the comment in
// language-appropriate syntax, e.g., "// " for C# or "# " for Python.
// Include any desired spacing in the prefix or suffix value.
public string CommentPrefix { get; set; } = "";
public string CommentSuffix { get; set; } = "";
public override bool Execute()
{
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
if (!string.IsNullOrEmpty(disableComments))
{
Log.LogMessage(MessageImportance.Normal, "Build comments disabled via environment variable.");
return true;
}
FileCounter counter = GetOrCreateCounter();
string buildDate = DateTime.UtcNow.ToString("yyyy-MM-dd");
string commentPattern = $@"^{Regex.Escape(CommentPrefix)}\s*Build Date:.*Version:.*{Regex.Escape(CommentSuffix)}$";
foreach (var item in TargetFiles)
{
AbsolutePath filePath = TaskEnvironment.GetAbsolutePath(item.ItemSpec);
try
{
string[] originalLines = File.ReadAllLines(filePath);
if (originalLines.Length > 0 && Regex.IsMatch(originalLines[0], commentPattern))
{
Log.LogMessage(MessageImportance.Low, $"Skipped (already annotated): {filePath}");
continue;
}
int fileNumber = counter.Next();
string comment = $"{CommentPrefix}Build Date: {buildDate}, Version: {VersionNumber}, File #: {fileNumber}{CommentSuffix}";
// Note: rewriting a file in place like this is convenient for a sample but is not
// recommended in production tasks. Prefer writing to a separate output file instead.
File.WriteAllLines(filePath, new[] { comment }.Concat(originalLines));
Log.LogMessage(MessageImportance.High, $"Added build comment to: {filePath}");
}
catch (Exception ex)
{
Log.LogError($"Failed to process {filePath}: {ex.Message}");
return false;
}
}
return true;
}
private FileCounter GetOrCreateCounter()
{
const string key = "BuildCommentTask.FileCounter";
var counter = BuildEngine4.GetRegisteredTaskObject(
key, RegisteredTaskObjectLifetime.Build) as FileCounter;
if (counter == null)
{
lock (s_counterLock)
{
counter = BuildEngine4.GetRegisteredTaskObject(
key, RegisteredTaskObjectLifetime.Build) as FileCounter;
if (counter == null)
{
counter = new FileCounter();
BuildEngine4.RegisterTaskObject(
key, counter,
RegisteredTaskObjectLifetime.Build,
allowEarlyCollection: false);
}
}
}
return counter;
}
}
}
Que se passe-t-il pour les tâches non migrées
Les tâches qui n’ont pas l’attribut [MSBuildMultiThreadableTask] ou qui n’implémentent IMultiThreadableTask pas continuent de fonctionner sans aucune modification. MSBuild exécute ces tâches dans un processus filiale TaskHost , qui fournit la même isolation au niveau du processus que les versions antérieures de MSBuild. Cette approche est plus lente en raison de la surcharge liée à la communication entre processus, mais elle est entièrement compatible avec le code de tâche existant. La migration est facultative pour la correction ( les tâches non migrées produisent toujours des résultats corrects), mais la migration améliore les performances de génération.
Prise en charge des versions antérieures de MSBuild
Si vous mettez à jour votre tâche personnalisée et que vous la distribuez à d’autres personnes, votre tâche prend en charge les clients à l’aide de MSBuild 18.6 ou version ultérieure. Pour prendre en charge les clients sur les versions antérieures de MSBuild, vous avez trois options.
Option 1 : Accepter des performances réduites
N’apportez aucune modification à votre tâche. MSBuild exécute des tâches non attribuées dans un processus de filiale TaskHost , ce qui est plus lent mais entièrement compatible. Cette option ne nécessite aucune modification du code.
Option 2 : Gérer des implémentations distinctes
Générez des assemblys de tâches distincts pour MSBuild 18.6+ et versions antérieures. La version MSBuild 18.6+ implémente IMultiThreadableTask et utilise TaskEnvironment. La version antérieure continue d’utiliser Task avec les API de niveau processus.
Option 3 : Pont de compatibilité
Définissez vous-même MSBuildMultiThreadableTaskAttribute dans votre assembly de tâches. Étant donné que MSBuild détecte l’attribut par espace de noms et par nom uniquement (ignorant l’assembly de définition), votre attribut autodéfini fonctionne à la fois dans les anciennes et nouvelles versions de MSBuild :
namespace Microsoft.Build.Framework
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}
Lors de l’exécution sur MSBuild 18.6 ou version ultérieure, MSBuild reconnaît l’attribut et exécute la tâche en cours d’exécution. Lors de l’exécution sur des versions antérieures, MSBuild ignore l’attribut inconnu et exécute la tâche comme avant.
Avec cette option, vous n’avez pas accès à TaskEnvironment, vous devrez donc gérer manuellement tout ce qu’il gère, comme convertir tous vos chemins relatifs en chemins absolus.
Comparaison des approches
Le tableau suivant compare les trois approches lors de l’exécution en mode multithread (-mt). En mode non multithread, toutes les tâches s’exécutent hors processus, quelle que soit la façon dont elles sont marquées.
| Approche | Maintenance | Performance (18.6+) | Performances (plus anciennes) | Accès à TaskEnvironment |
|---|---|---|---|---|
| Implémentations distinctes | Élevé | Entièrement en cours | Entièrement hors processus | Oui (version 18.6+ ) |
| Pont de compatibilité | Faible | Entièrement en cours de traitement | Entièrement hors processus | Non (attribut uniquement) |
| Aucune modification | None | Sidecar (lent) | Entièrement hors processus | No |