Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
MSBuild 18.6 führt die Funktion zum parallelen Erstellen innerhalb desselben Prozesses ein. Um diesen Modus zu aktivieren, geben Sie die Befehlszeilenoption -mt an. Frühere Versionen von MSBuild unterstützten parallele Builds, aber Builds wurden in separaten Prozessen durchgeführt. Diese Änderung wirkt sich auf die Erstellung von Aufgaben aus. Während zuvor Aufgaben in einem separaten Prozess ausgeführt würden, werden nun alle Multithread-fähigen Aufgaben im selben Prozess ausgeführt. Während sich die meisten Logik nicht ändern müssen, gibt es einige Prozessebenenkonstrukte, die sorgfältiger behandelt werden müssen. Prozessebenenkonstrukte umfassen das aktuelle Arbeitsverzeichnis, Umgebungsvariablen und Prozessstartinformationen (ProcessStartInfo).
Um diese Änderungen zu unterstützen, führt MSBuild 18.6 die schnittstelle IMultiThreadableTask (in Microsoft.Build.Framework) und die klasse TaskEnvironment ein.
TaskEnvironment enthält eine ProjectDirectory Eigenschaft und Methoden wie GetAbsolutePath(), , GetEnvironmentVariable(), SetEnvironmentVariable()und GetProcessStartInfo().
Von Bedeutung
Der Multithread-Modus ist derzeit als experimentelle Funktion verfügbar; er wird derzeit nicht für den produktiven Einsatz empfohlen. Das Aktualisieren Ihrer MSBuild-Bibliotheksabhängigkeiten für die Verwendung der APIs für den Multithreadmodus führt implizit dazu, dass Ihre Bibliotheken unter älteren Versionen von Visual Studio und MSBuild nicht mehr ausgeführt werden können. Wir empfehlen Early Adopters, den Multithread-Modus zu testen und Feedback zu geben. Übermitteln Sie Probleme beim Repository MSBuild GitHub.
Die IMultiThreadableTask Schnittstelle definiert den Vertrag für Aufgaben, die in Multithread-Builds ausgeführt werden können:
// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
TaskEnvironment TaskEnvironment { get; set; }
}
Um eine Aufgabe zu migrieren, implementieren Sie IMultiThreadableTask neben Ihrer vorhandenen Task Basisklasse und machen Sie die TaskEnvironment Eigenschaft verfügbar:
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;
// ...
}
Aufgaben, die IMultiThreadableTask implementieren, können im selben Prozess ausgeführt werden. Alle diese Tasks müssen außerdem das Attribut [MSBuildMultiThreadableTask] tragen, das als Kennzeichen dient, mit dem MSBuild den Task für die prozessinterne Ausführung kennzeichnet. Vergewissern Sie sich vor dem Hinzufügen des Attributs, dass die Aufgabe keine Abhängigkeiten von Prozessebenenkonstrukten wie dem aktuellen Arbeitsverzeichnis oder der Umgebung hat und dass der Code threadsicher ist. Achten Sie besonders darauf, den threadsicheren Zugriff auf statische Variablen sicherzustellen, da diese Variablen von allen Aufgabeninstanzen gemeinsam verwendet werden und möglicherweise von verschiedenen Instanzen der Aufgabe, die ebenfalls im selben Prozess ausgeführt werden, aufgerufen oder geändert werden können.
Beispielaufgabe: BuildCommentTask
Das folgende Beispiel AddBuildCommentTask wird in diesem Artikel verwendet, um den Migrationsprozess zu veranschaulichen. Diese Aufgabe stellt einen Buildkommentar in Textdateien vor. Standardmäßig schreibt er Nur-Text; mit den optionalen Eigenschaften CommentPrefix und CommentSuffix können Aufrufer den Kommentar in sprachgerechte Syntax umschließen (z. B. // für C#, <!-- und --> für XML, # für Python oder 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;
}
}
}
Eine Projektdatei ruft diese Aufgabe möglicherweise für verschiedene Dateitypen auf, wobei die entsprechende Kommentarsyntax für jede übergeben wird:
<!-- 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=" -->" />
Diese Aufgabe enthält vier Threadsicherheitsprobleme, die für Multithread-Builds behoben werden müssen:
-
Relative Pfade:
File.ReadAllLinesundFile.WriteAllLinesverwendenitem.ItemSpecdirekt, der ein relativer Pfad sein könnte. Im Multithread-Modus ist das Arbeitsverzeichnis des Prozesses nicht garantiert das Projektverzeichnis. -
Statisches Feld:
ModifiedFileCountist einstaticFeld, das für alle Instanzen freigegeben wird, was dazu führt, dass Datenrennen gleichzeitig ausgeführt werden, wenn mehrere Builds gleichzeitig ausgeführt werden. -
Umgebungsvariablen: Das häufigste Problem mit Umgebungsvariablen in Multithread-Builds sind Aufgaben, die vor dem Starten eines untergeordneten Prozesses Umgebungsvariablen festlegen, in der Erwartung, dass der untergeordnete Prozess diese erbt. Im Multithread-Modus verändert
Environment.SetEnvironmentVariable()die auf Prozessebene gemeinsam von allen parallel ausgeführten Builds genutzte Umgebung, sodass sich eine für den untergeordneten Prozess eines Projekts gedachte Änderung auf den eines anderen auswirken kann. Das direkte Lesen von Umgebungsvariablen im Aufgabencode (Environment.GetEnvironmentVariable()) ist im Allgemeinen auch eine schlechte Methode; MSBuild-Eigenschaften sind eine bessere Alternative, da sie protokolliert und nachverfolgbar sind.
Von Bedeutung
Der Multithread-Buildmodus ist derzeit nur für CLI- (dotnet build und MSBuild.exe) Builds verfügbar. Visual Studio MSBuild-Builds unterstützen noch keine Multithreadausführung im Prozess. In Visual Studio wird die gesamte Aufgabenausführung weiterhin außerhalb des Prozesses ausgeführt. Visual Studio Integration ist für eine zukünftige Version geplant.
Voraussetzungen
MSBuild 18.6 oder höher.
Aktivieren sie die Ausführung von Multithread-Aufgaben mit dem
-mtBefehlszeilenschalter:dotnet build -mtWeitere Informationen zum
-mtSwitch finden Sie unter MSBuild-Befehlszeilenreferenz.
Planung der Migration
Überprüfen Sie Den Aufgabencode für die folgenden Probleme:
- Überprüfen Sie den Aufgabencode, und identifizieren Sie die Verwendung relativer Pfade. Überprüfen Sie alle Eingabe- und Datei-E/A-Vorgänge.
- Überprüfen Sie, ob Umgebungsvariablen verwendet werden.
- Überprüfen Sie die Nutzung der
ProcessStartInfoAPI. - Überprüfen Sie statische Felder oder Datenstrukturen, und verwenden Sie Standardmethoden, um sie threadsicher zu machen.
- Wenn keines der oben genannten Punkte zutrifft, sollten Sie das Attribut nur hinzufügen.
- Berücksichtigen Sie spezielle Anforderungen für die Unterstützung früherer Versionen von MSBuild. Siehe Support für frühere Versionen von MSBuild.
Kurzübersicht zum API-Ersatz
In der folgenden Tabelle sind die .NET APIs zusammengefasst, die Sie ersetzen sollten, und deren TaskEnvironment Entsprechungen:
| Zu vermeidende .NET-API | Ebene | Ersetzung |
|---|---|---|
Path.GetFullPath(path) |
FEHLER | Siehe Hinweis unter dieser Tabelle. |
File.* mit relativen Pfaden |
FEHLER | Zuerst mit TaskEnvironment.GetAbsolutePath() auflösen |
Directory.* mit relativen Pfaden |
FEHLER | Zuerst mit TaskEnvironment.GetAbsolutePath() auflösen |
Environment.GetEnvironmentVariable() |
FEHLER | TaskEnvironment.GetEnvironmentVariable() |
Environment.SetEnvironmentVariable() |
FEHLER | TaskEnvironment.SetEnvironmentVariable() |
Environment.CurrentDirectory |
FEHLER | TaskEnvironment.ProjectDirectory |
new ProcessStartInfo() |
FEHLER | TaskEnvironment.GetProcessStartInfo() |
Process.Start() |
FEHLER | Verwenden Sie ToolTask oder TaskEnvironment.GetProcessStartInfo() |
| Statische Felder | WARNUNG | Verwenden Sie Instanzfelder oder threadsichere Sammlungen |
Note
Path.GetFullPath(path) führt zwei Dinge aus: Sie konvertiert einen relativen Pfad in einen absoluten Pfad und erzeugt eine kanonische Form des Pfads (Auflösen . und .. Segmente). Diese müssen separat behandelt werden:
-
Nur absoluter Pfad: Verwenden Sie
TaskEnvironment.GetAbsolutePath(path). Dieser Ansatz reicht für die meisten Datei-E/A-Vorgänge aus, bei denen Sie den Pfad direkt an .NET-APIs übergeben. -
Kanonischer Pfad: Wenn Sie sich auf die kanonische Form verlassen (z. B. wenn Sie einen Pfad als Cache- oder Wörterbuchschlüssel verwenden), verwenden Sie dies
Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path)), um einen vollständig aufgelösten, kanonischen absoluten Pfad abzurufen.
Markieren Sie die Aufgabe mit dem Attribut
Alle Aufgaben, die an Multithread-Builds teilnehmen, müssen mit dem [MSBuildMultiThreadableTask] Attribut gekennzeichnet werden. Dieses Attribut ist das Signal, das MSBuild verwendet, um Aufgaben zu identifizieren, die sicher im Prozess ausgeführt werden können.
[MSBuildMultiThreadableTask]
public class MyTask : Task
{
public override bool Execute()
{
// Task logic that doesn't depend on process-level state
return true;
}
}
Wenn Ihre Aufgabe bereits thread-safe ist und keine prozessbezogenen APIs verwendet (aktuelles Arbeitsverzeichnis, Umgebungsvariablen, ProcessStartInfo), genügt das Attribut allein. Die Aufgabe erbt weiterhin von Task (oder ToolTask), ohne weitere Änderungen.
Wenn Ihre Aufgabe API-Aufrufe auf Prozessebene ersetzen muss (z. B. um relative Pfade aufzulösen oder Umgebungsvariablen sicher zu lesen), implementieren Sie IMultiThreadableTaskebenfalls . Diese Schnittstelle bietet Ihrer Aufgabe Zugriff auf die TaskEnvironment Eigenschaft. Das Attribut bleibt in beiden Fällen erforderlich; IMultiThreadableTask ist ein zusätzlicher Schritt, der die TaskEnvironment API freischaltet.
Note
MSBuild erkennt MSBuildMultiThreadableTaskAttribute nur anhand von Namespace und Namen und ignoriert dabei die definierende Assembly. Dies bedeutet, dass Sie das Attribut selbst in Ihrem eigenen Code definieren können (siehe Unterstützung früherer Versionen von MSBuild) und MSBuild erkennt es weiterhin.
Note
Dies MSBuildMultiThreadableTaskAttribute ist nicht vererbbar (Inherited = false). Jede Aufgabenklasse muss das Attribut explizit als multithreadfähig deklarieren. Die Ableitung von einer Klasse, die dieses Attribut besitzt, führt nicht automatisch dazu, dass die abgeleitete Klasse multithreadfähig ist.
Initialisieren von TaskEnvironment in Fallback
Wenn Sie IMultiThreadableTask implementieren, initialisieren Sie die TaskEnvironment-Eigenschaft auf TaskEnvironment.Fallback:
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
MSBuild legt diese Eigenschaft vor dem Aufrufen Execute() in einem normalen Build fest. Der Standardwert Fallback stellt sicher, dass die Aufgabe in anderen Hosting-Szenarien (z. B. Unit-Tests oder benutzerdefinierten Build-Orchestrierungstools) korrekt funktioniert, in denen MSBuild nicht vorhanden ist, um die Eigenschaft zu setzen. Ohne sie würde der Zugriff auf TaskEnvironment außerhalb der Engine eine Nullverweisausnahme auslösen.
Wenn Sie MSBuild-Versionen vor 18.6 unterstützen müssen, die TaskEnvironment.Fallback nicht enthalten, initialisieren Sie die Eigenschaft stattdessen mit null und sichern Sie alle Aufrufe von TaskEnvironment mit einer Nullprüfung ab. Weitere Optionen finden Sie unter "Support für frühere Versionen von MSBuild ".
Aktualisierung von Pfaden und Datei-E/A
Eine Aufgabe akzeptiert häufig Eingaben, z. B. Elementlisten in MSBuild, die sich bei Dateien möglicherweise in Form relativer Pfade befinden.
Relative Pfade sind immer relativ zum aktuellen Arbeitsverzeichnis des Prozesses, aber da die Aufgabe jetzt im Prozess ausgeführt wird, ist das Arbeitsverzeichnis möglicherweise nicht identisch mit dem, als die Aufgabe in einem eigenen Prozess ausgeführt wurde. Solche Pfade sind relativ zum Projektverzeichnis. Dies TaskEnvironment umfasst eine ProjectDirectory Eigenschaft und eine GetAbsolutePath() Methode, die Sie verwenden können, um relative Pfade in absolute Pfade aufzulösen. Sie können auch auf das FullPath Metadatum zugreifen. Es ist nicht erforderlich, den ItemSpec relativen Pfad zu verwenden und es dann absolutisieren.
Der AbsolutePath-Typ
AbsolutePath ist eine schreibgeschützte Struktur in Microsoft.Build.Framework, die einen überprüften absoluten Dateipfad darstellt. Zu den wichtigsten Mitgliedern gehören:
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);
}
Der AbsolutePath Konstruktor überprüft, ob der bereitgestellte Pfad gerootet ist. Sie können auch einen AbsolutePath erstellen, indem Sie einen relativen Pfad und einen Basispfad angeben. Die implizite Konvertierung zu string bedeutet, dass Sie ein AbsolutePath direkt an eine beliebige API übergeben können, die einen string-Pfad erwartet.
Die OriginalValue Eigenschaft bewahrt die ursprüngliche Pfadzeichenfolge in der Form, in der sie vor der Auflösung übergeben wurde. Diese Eigenschaft ist nützlich, wenn Sie relative Pfade in Vorgangsausgabeen oder Protokollmeldungen beibehalten müssen. Beispielsweise kann eine Aufgabe, die protokolliert, welche Dateien sie verarbeitet hat, in ihren Protokollmeldungen OriginalValue verwenden, sodass Pfade in der Ausgabe relativ und lesbar bleiben, während sie für die eigentlichen Datei-E/A-Vorgänge weiterhin die aufgelöste Value (oder die implizite string-Konvertierung) verwendet.
Verwenden Sie TaskEnvironment.GetAbsolutePath(), um Elementpfade aufzulösen:
Vorher:
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));
Nachher:
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}");
Behandeln von Dateikonflikten in parallelen Builds
Dateikonflikt kann auftreten, wenn mehrere Aufgaben parallel ausgeführt werden und auf dieselbe Datei zugreifen. Diese Problematik gilt sowohl für das herkömmliche Multiprozessmodell als auch für den neueren Multithreadingmodus innerhalb eines Prozesses. In beiden Fällen kann gleichzeitig auf dieselbe Datei zugegriffen werden, wenn:
- Dieselbe Datei wird in mehreren Unterprojektbuilds angezeigt (z. B. einer freigegebenen Konfigurationsdatei oder einer verknüpften Quelldatei).
- Eine Aufgabe liest und schreibt eine Datei, die auch von einer anderen Aufgabeninstanz verarbeitet wird.
Komfortmethoden wie File.ReadAllLines und File.WriteAllLines bieten keine explizite Kontrolle über die Dateisperre. Wenn gleichzeitiger Zugriff möglich ist, verwenden Sie FileStream mit expliziter gemeinsamer Nutzung und Sperren:
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);
}
Wichtige Richtlinien für Datei-Ein-/Ausgabe in Multithreading-Aufgaben:
- Verwenden Sie
FileShare.Nonefür Lese-Ändern-Schreibvorgänge. Diese Einstellung verhindert, dass eine andere Aufgabe veraltete Inhalte liest, während Sie die Datei aktualisieren. - Fangen Sie
IOExceptionnach, und versuchen Sie es erneut. Wenn eine andere Aufgabe oder ein anderer Prozess eine Sperre hält, wirft Ihr ÖffnungsversuchIOException. Ein kurzer Wiederholungsversuch mit Backoff ist oft sinnvoll. - Vermeiden Sie gleichzeitiges Halten von Sperren für mehrere Dateien. Wenn zwei Aufgaben jeweils eine Datei sperren und dann versuchen, die andere zu sperren, erhalten Sie einen Deadlock. Wenn Sie mehrere Dateien verwenden müssen, sperren Sie sie in einer konsistenten Reihenfolge (z. B. sortiert nach vollständigem Pfad).
- Halten Sie Sperren so kurz wie möglich. Öffnen Sie die Datei, lesen, ändern, schreiben und schließen Sie sie in einem Vorgang. Halten Sie eine Dateisperre nicht gedrückt, während Sie keine verwandte Arbeit ausführen.
Das vorangehende Beispiel ist ein Ansatz. Allgemeine Anleitungen zur threadsicheren Datei-E/A in .NET finden Sie unter FileStream-Klasse, FileShare-Enumeration und Managed threading best practices.
Note
TaskEnvironment selbst ist nicht threadsicher. Das ist nur wichtig, wenn Ihre Aufgabe intern eigene Threads startet (z. B. mit Parallel.ForEach oder Task.Run). Die meisten Aufgaben tun dies nicht. Sie implementieren Execute() linear und lassen MSBuild Parallelität über Aufgabeninstanzen hinweg verarbeiten. Wenn Ihre Aufgabe eigene Threads erstellt, kopieren Sie Werte aus TaskEnvironment in lokale Variablen, bevor Sie diese starten, anstatt von mehreren Threads gleichzeitig auf TaskEnvironment zuzugreifen.
Aktualisieren von Umgebungsvariablen
Note
Das Lesen von Umgebungsvariablen im Aufgabencode ist in der Regel eine schlechte Methode, auch in Singlethread-Builds. MSBuild-Eigenschaften sind eine bessere Alternative: Sie sind explizit auf den Build ausgerichtet, während des Builds protokolliert und im Buildprotokoll nachverfolgbar. Wenn Ihre Aufgabe zurzeit eine Umgebungsvariable liest, um Eingaben zu empfangen, sollten Sie sie stattdessen durch eine Aufgabeneigenschaft ersetzen. Das Projekt kann den Wert trotzdem von einer Umgebungsvariablen ableiten: <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.
Die Anleitung in diesem Abschnitt besteht darin, vorhandene Aufgaben zu migrieren, die bereits auf Umgebungsvariablen basieren. Wenn Sie die Möglichkeit haben, umzugestalten, bevorzugen Sie Eigenschaften und Elemente.
Festlegen von Umgebungsvariablen für untergeordnete Prozesse
Das häufigste Problem mit Umgebungsvariablen in Builds mit mehreren Threads ist eine Aufgabe, die eine Umgebungsvariable setzt und dann einen Kindprozess startet, in der Erwartung, dass der Kindprozess sie erbt. Im Multiprozessmodell Environment.SetEnvironmentVariable() wurde die Arbeitsprozessumgebung für dieses Projekt sicher geändert. Im Multithread-Modus wird der Prozess für alle gleichzeitigen Builds freigegeben, sodass eine Änderung, die für den untergeordneten Prozess eines Projekts vorgesehen ist, in einen anderen ablaufen kann.
Verwenden Sie TaskEnvironment.SetEnvironmentVariable() zusammen mit TaskEnvironment.GetProcessStartInfo() (siehe Update ProcessStart-API-Aufrufe).
GetProcessStartInfo() gibt ein ProcessStartInfo zurück, das vorab mit dem Arbeitsverzeichnis des Projekts und seiner isolierten Umgebungstabelle befüllt ist, einschließlich aller Variablen, die Sie mit SetEnvironmentVariable() festlegen, sodass Kindprozesse automatisch die korrekte, projektbezogene Umgebung erben.
Vorher:
Environment.SetEnvironmentVariable("TOOL_OUTPUT_DIR", outputDir);
var startInfo = new ProcessStartInfo("mytool.exe") { UseShellExecute = false };
Process.Start(startInfo); // inherits the modified process-level environment
Nachher:
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
Lesen von Umgebungsvariablen in vorhandenen Aufgaben
Wenn Ihre vorhandene Aufgabe Umgebungsvariablen liest und Sie nicht sofort auf Aufgabeneigenschaften umstellen können, ersetzen Sie Environment.GetEnvironmentVariable() durch TaskEnvironment.GetEnvironmentVariable(). Dieser Methodenaufruf liest aus der projektbezogenen Umgebungstabelle statt aus der gemeinsamen Prozessumgebung, sodass gleichzeitige Builds sich nicht gegenseitig beeinträchtigen.
Vor (von BuildCommentTask):
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Nachher:
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Tipp
Wenn Sie vorhandenen Code aktualisieren, der eine Umgebungsvariable liest, sollten Sie das Muster durch eine Aufgabeneigenschaft ersetzen. Beispielsweise stellen Sie in der Aufgabe public bool DisableComments { get; set; } bereit und lassen das Projekt DisableComments="$(DISABLE_BUILD_COMMENTS)" übergeben. MSBuild protokolliert den aufgelösten Wert, wodurch er im Buildprotokoll sichtbar wird und sich deutlich leichter diagnostizieren lässt als das Auslesen einer versteckten Umgebungsvariablen.
Aktualisieren von ProcessStart-API-Aufrufen
Typischerweise sollten Sie, wenn eine Aufgabe einen Prozess startet, ToolTask verwenden, das alles für Sie übernimmt. Wenn Sie eine Aufgabe aktualisieren, die ProcessStartInfo direkt aufruft, verwenden Sie TaskEnvironment.GetProcessStartInfo(). Dies gibt ein ProcessStartInfo konfiguriertes Arbeitsverzeichnis des Projekts und seine isolierte Umgebungstabelle zurück. Wenn Sie vor dem Start auch Umgebungsvariablen festlegen, verwenden Sie TaskEnvironment.SetEnvironmentVariable() zuerst, wie im vorherigen Abschnitt gezeigt.
Vorher:
var startInfo = new ProcessStartInfo("mytool.exe")
{
WorkingDirectory = ".",
UseShellExecute = false
};
Process.Start(startInfo);
Nachher:
ProcessStartInfo startInfo = TaskEnvironment.GetProcessStartInfo();
startInfo.FileName = "mytool.exe";
startInfo.UseShellExecute = false;
Process.Start(startInfo);
Note
Wenn Ihr Task von ToolTask erbt, werden Informationen zum Prozessstart bereits für Sie verarbeitet. Sie müssen nur Aufgaben aktualisieren, die ProcessStartInfo direkt erstellen.
Statische Felder und Datenstrukturen threadsicher machen
Statische Felder erfordern eine sorgfältige Behandlung, wenn Sie zu Multithread-Builds migrieren. Selbst im Multiprozessmodell kann ein einzelner Prozess mehrere Projekte bauen, sodass der statische Zustand gemeinsam genutzt wird, jedoch nicht gleichzeitig.
Der Multithreading-Modus verleiht diesem Problem eine neue Dimension. Mehrere Builds können jetzt denselben Prozess gemeinsam nutzen und Aufgaben gleichzeitig ausführen (insbesondere mit MSBuild Server, der automatisch mit Multithreading aktiviert wird). Ein statisches Feld wird für alle Aufgabeninstanzen im Prozess freigegeben, nicht nur innerhalb Ihres Builds, sondern potenziell über separate Buildaufrufe, die gleichzeitig ausgeführt werden. Beispielsweise könnten zwei Entwickler, die dotnet build gleichzeitig auf einem Buildserver ausführen, oder zwei Terminalfenster auf demselben Rechner sich denselben statischen Zustand teilen, wodurch diese Builds gleichzeitig darauf zugreifen.
BuildCommentTask Im Beispiel wird das statische Feld ModifiedFileCount für alle Instanzen freigegeben:
Vorher:
private static int ModifiedFileCount = 0;
// In Execute():
ModifiedFileCount++;
Dieser Code hat zwei Probleme. Erstens ist der ++ Operator nicht atomisch. Wenn mehrere Aufgabeninstanzen gleichzeitig ausgeführt werden, können zwei Threads denselben Wert lesen und beide dasselbe inkrementierte Ergebnis schreiben, was zu verlorenen Zählungen führt. Zweitens, da das Feld statisch ist, wird es über Builds hinweg beibehalten und zwischen gleichzeitigen Builds im selben Prozess gemeinsam genutzt.
In den folgenden Abschnitten werden zwei Ansätze zum Beheben dieser Probleme dargestellt, von der einfachsten bis zur korrektsten.
Ansatz 1: Verwenden einer threadsicheren, aber prozessweiten API
Die einfachste Lösung besteht darin, die Inkrementierung atomar zu machen:
private static int ModifiedFileCount = 0;
// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);
Interlocked.Increment führt den Lese-Inkrement-Schreibvorgang als einzelnen atomischen Vorgang aus, sodass keine Anzahl verloren geht. Dieser Ansatz löst das Parallelitätsproblem, aber der Zähler wird weiterhin für alle Builds im Prozess freigegeben, einschließlich aufeinander folgender Builds und gleichzeitiger Builds. Wenn zwei Builds gleichzeitig ausgeführt werden, werden ihre Dateinummern verschachtelt vergeben (Build A erhält #1, #3, #5; Build B erhält #2, #4, #6). Ob diese Situation akzeptabel ist, hängt davon ab, ob Ihre Aufgabe eine build-spezifische Isolation erfordert. Für einen sequenziellen Dateinummerierungszähler wie ModifiedFileCount ist die buildübergreifende gemeinsame Nutzung ein Korrektheitsproblem; verwenden Sie stattdessen RegisterTaskObject (siehe Ansatz 2).
Hier ist das threadsichere, aber prozessweite API-Äquivalent InterlockedIncrement, aber in Ihrem eigenen Code müssten Sie geeignete threadsichere Alternativen für alle APIs finden, die nicht threadsicher sind. Wenn Ihre Aufgabe beispielsweise den Zustand mithilfe eines Dictionary speichert, sollten Sie die Verwendung von ConcurrentDictionary<TKey,TValue> in Betracht ziehen.
Ansatz 2: RegisterTaskObject für buildbezogene Isolation
Wenn Ihre Aufgabe einen statischen Zustand benötigt, der von Unterprojekten während eines einzelnen Build-Aufrufs gemeinsam genutzt wird, aber von anderen gleichzeitig ausgeführten Builds isoliert ist, verwenden Sie IBuildEngine4.RegisterTaskObject mit RegisteredTaskObjectLifetime.Build. MSBuild verwaltet die Lebensdauer des Objekts, das bei der ersten Verwendung erstellt und bereinigt wird, wenn der Build endet. Beachten Sie, dass die registrierten Objekte threadsicher sein müssen.
Definieren Sie zunächst eine einfache threadsichere Zählerklasse:
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
Verwenden Sie dann eine Hilfsmethode mit doppelgecheckter Sperre, um den Zähler abzurufen oder zu erstellen:
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;
}
In Execute():
FileCounter counter = GetOrCreateCounter();
// ...
int fileNumber = counter.Next();
Bei diesem Ansatz erhält jeder Buildaufruf einen eigenen FileCounter. Alle Teilprojekte innerhalb desselben Builds verwenden denselben Zähler (fortlaufende Nummerierung), aber ein separater, gleichzeitig auf demselben Rechner laufender dotnet build-Prozess erhält einen anderen Zähler.
RegisteredTaskObjectLifetime.Build weist MSBuild an, das Objekt auf den aktuellen Buildaufruf zu beschränken und zu bereinigen, wenn der Build endet.
Wählen Sie den richtigen Ansatz
Wenn Sie entscheiden, wie der statische Zustand behandelt werden soll, beginnen Sie mit dieser Frage: Sind diese Daten sicher, über alle Builds hinweg freizugeben, die jemals im selben Prozess ausgeführt werden können, einschließlich aufeinander folgender Builds und gleichzeitiger Builds?
MSBuild-Arbeitsprozesse bleiben über Aufrufe hinweg bestehen (die Wiederverwendung von Knoten ist standardmäßig aktiviert), und ein MSBuild-Prozess kann mehrere Lösungsbuilds während seiner Lebensdauer bereitstellen, nicht nur innerhalb eines einzigen dotnet build Aufrufs. Gehen Sie nicht davon aus, dass ein Prozess nur einen Build behandelt.
Nutzen Sie diese Richtlinien:
- Behalten Sie das statische Feld nur bei, wenn auf die zwischengespeicherten Daten von mehreren Threads in verschiedenen Projekten und über mehrere Builds hinweg sicher zugegriffen werden kann, ohne dass zwischen den Builds eine Invalidierung erforderlich ist. Beispielsweise kann ein Cache mit unveränderlichen Daten, die einmal aus Eingaben berechnet wurden, die sich nie ändern (z. B. Assemblymetadaten, die einmal beim Start geladen wurden) qualifizieren.
-
Verwenden Sie
IBuildEngine4.RegisterTaskObjectmitRegisteredTaskObjectLifetime.Build, wenn der Zustand für jeden Build-Aufruf isoliert sein muss (z. B. Zähler, Akkumulatoren oder Caches, die zwischen Builds zurückgesetzt werden sollen oder nicht zwischen gleichzeitigen Builds übergreifen dürfen). Dies ist der bevorzugte Ansatz für den meisten gemeinsam genutzten änderbaren Zustand. -
Verwenden Sie
System.ThreadingGrundtypen (Interlocked,ConcurrentDictionary,lock,ReaderWriterLockSlim), um alle beibehaltenen statischen Zustand threadsicher zu machen, denken Sie jedoch daran, dass Threadsicherheit allein keine Isolation auf Buildebene bietet. Siehe Bewährte Methoden für verwaltetes Threading.
Tipp
Das vollständige Migrationsbeispiel später in diesem Artikel verwendet den RegisterTaskObject-Ansatz, um die auf den Buildbereich beschränkte Isolation zu veranschaulichen.
Vollständiges Migrationsbeispiel
Der folgende Code zeigt das vollständig migrierte AddBuildCommentTask, bei dem alle fünf Änderungen angewendet wurden:
- Besitzt das
[MSBuildMultiThreadableTask]-Attribut, das die Ausführung im Prozess kennzeichnet. -
IMultiThreadableTaskImplementiert neben der vorhandenenTaskBasisklasse und macht dieTaskEnvironmentEigenschaft verfügbar. - Verwendet
TaskEnvironment.GetAbsolutePath()zur Pfadauflösung. - Verwendet
TaskEnvironment.GetEnvironmentVariable()anstelle vonEnvironment.GetEnvironmentVariable(). - Verwendet
IBuildEngine4.RegisterTaskObjectmitRegisteredTaskObjectLifetime.Build, um den Dateizähler auf den aktuellen Build-Aufruf zu beschränken und dadurch den prozessweiten statischen Zähler zu ersetzen.
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;
}
}
}
Was geschieht mit nicht migrierten Aufgaben
Aufgaben, die nicht über das [MSBuildMultiThreadableTask] Attribut verfügen oder nicht implementieren IMultiThreadableTask , funktionieren weiterhin ohne Änderungen. MSBuild führt diese Aufgaben in einem untergeordneten TaskHost Prozess aus, der die gleiche Isolation auf Prozessebene wie frühere Versionen von MSBuild bereitstellt. Dieser Ansatz ist aufgrund des Mehraufwands für die Kommunikation zwischen Prozessen langsamer, ist aber vollständig mit vorhandenem Aufgabencode kompatibel. Die Migration ist für die Korrektheit nicht erforderlich – nicht migrierte Aufgaben liefern weiterhin korrekte Ergebnisse –, aber eine Migration verbessert die Build-Performance.
Unterstützung früherer Versionen von MSBuild
Wenn Sie Ihre benutzerdefinierte Aufgabe aktualisieren und sie dann an andere Personen verteilen, unterstützt Ihre Aufgabe Clients mit MSBuild 18.6 oder höher. Um Clients in früheren Versionen von MSBuild zu unterstützen, haben Sie drei Optionen.
Option 1: Nehmen Sie eine verringerte Leistung in Kauf
Nehmen Sie keine Änderungen an Ihrer Aufgabe vor. MSBuild führt nicht zugeordnete Aufgaben in einem untergeordneten TaskHost Prozess aus, was langsamer, aber vollständig kompatibel ist. Für diese Option sind keine Codeänderungen erforderlich.
Option 2: Verwalten separater Implementierungen
Erstellen Sie separate Aufgabenassemblys für MSBuild 18.6+ und frühere Versionen. Die MSBuild 18.6+-Version implementiert IMultiThreadableTask und verwendet TaskEnvironment. Die frühere Version verwendet weiterhin Task mit APIs auf Prozessebene.
Option 3: Kompatibilitätsbrücke
Definieren Sie MSBuildMultiThreadableTaskAttribute selbst in Ihrer Aufgabenassembly. Da MSBuild das Attribut nur nach Namespace und Name erkennt (wobei die definierende Assembly ignoriert wird), funktioniert Ihr selbstdefiniertes Attribut sowohl in alten als auch neuen Versionen von MSBuild:
namespace Microsoft.Build.Framework
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}
Wenn msBuild 18.6 oder höher ausgeführt wird, erkennt MSBuild das Attribut und führt die Aufgabe im Prozess aus. Bei der Ausführung in früheren Versionen ignoriert MSBuild das unbekannte Attribut und führt die Aufgabe wie zuvor aus.
Mit dieser Option haben Sie keinen Zugriff auf TaskEnvironment, sodass Sie alles, was TaskEnvironment übernimmt, manuell erledigen müssen, z. B. die Umwandlung aller relativen Pfade in absolute Pfade.
Vergleich von Ansätzen
In der folgenden Tabelle werden die drei Ansätze verglichen, wenn sie im Multithreadmodus (-mt) ausgeführt werden. Im Singlethread-Modus werden alle Aufgaben in separaten Prozessen ausgeführt, unabhängig davon, wie sie gekennzeichnet sind.
| Ansatz | Maintenance | Leistung (18,6+) | Leistungsfähigkeit (älter) | TaskEnvironment-Zugriff |
|---|---|---|---|---|
| Separate Implementierungen | Hoch | Vollständig in Bearbeitung | Vollständiger Out-of-Process | Ja (18.6+ Version) |
| Kompatibilitätsbrücke | Niedrig | Vollständig in Bearbeitung | Vollständig außerhalb des Prozesses | Nein (nur Attribut) |
| Keine Änderungen | Nichts | Sidecar (langsamer) | Vollständiger Out-of-Process | No |