Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
MSBuild 18.6 introduceert de mogelijkheid om parallel te bouwen binnen hetzelfde proces. Als u zich wilt aanmelden voor deze modus, geeft u de -mt opdrachtregelswitch door. Eerdere versies van MSBuild ondersteund parallelle builds, maar builds werden uitgevoerd in afzonderlijke processen. Deze wijziging heeft enkele gevolgen voor de wijze waarop u taken ontwerpt. Voorheen werden taken in een afzonderlijk proces uitgevoerd, maar nu worden alle multithread-ondersteunde taken in hetzelfde proces uitgevoerd. Hoewel de meeste logica niet hoeft te worden gewijzigd, zijn er enkele constructies op procesniveau die zorgvuldiger moeten worden verwerkt. Constructies op procesniveau omvatten de huidige werkmap, omgevingsvariabelen en processtartgegevens (ProcessStartInfo).
Ter ondersteuning van deze wijzigingen introduceert MSBuild 18.6 de interface IMultiThreadableTask (in Microsoft.Build.Framework) en de klasse TaskEnvironment.
TaskEnvironment bevat een ProjectDirectory eigenschap en methoden zoals GetAbsolutePath(), GetEnvironmentVariable(), SetEnvironmentVariable()en GetProcessStartInfo().
Belangrijk
Multithreaded-modus is momenteel beschikbaar als experimentele functie; het wordt op dit moment niet aanbevolen voor productiegebruik. Als u de afhankelijkheden van uw MSBuild-bibliotheek bijwerkt om de multithreaded-modus-API's te gebruiken, voorkomt u impliciet dat uw bibliotheken worden uitgevoerd in oudere versies van Visual Studio en MSBuild. We raden early adopters aan om de multithreaded-modus uit te proberen en ons feedback te geven. Verzend problemen in de opslagplaats MSBuild GitHub.
De IMultiThreadableTask interface definieert het contract voor taken die in het proces kunnen worden uitgevoerd in multithreaded-builds:
// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
TaskEnvironment TaskEnvironment { get; set; }
}
Als u een taak wilt migreren, implementeer IMultiThreadableTask naast uw bestaande Task-basisklasse en maak de TaskEnvironment-eigenschap beschikbaar:
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;
// ...
}
Taken die IMultiThreadableTask implementeren, kunnen in-process worden uitgevoerd. Al deze taken moeten ook het attribuut [MSBuildMultiThreadableTask] hebben, de markering die MSBuild gebruikt om de taak aan te merken voor uitvoering in het proces. Voordat u het kenmerk toevoegt, controleert u of de taak geen afhankelijkheden heeft van constructies op procesniveau, zoals de huidige werkmap of de omgeving, en of de bijbehorende code thread-safe is. Let met name op het garanderen van threadveilige toegang tot statische variabelen, omdat deze variabelen worden gedeeld tussen alle taakexemplaren en kunnen worden geopend of gewijzigd door verschillende exemplaren van de taak die ook in hetzelfde proces worden uitgevoerd.
Voorbeeldtaak: BuildCommentTask
Het volgende voorbeeld AddBuildCommentTask wordt in dit artikel gebruikt om het migratieproces te illustreren. Deze taak voegt een buildopmerking toe aan het begin van tekstbestanden. Standaard wordt tekst zonder opmaak geschreven; met de optionele eigenschappen CommentPrefix en CommentSuffix kunnen bellers de opmerking in de juiste syntaxis verpakken (bijvoorbeeld // voor C#, <!-- en --> voor XML, # voor Python of 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;
}
}
}
Een projectbestand kan deze taak aanroepen voor verschillende bestandstypen, waarbij de juiste syntaxis voor opmerkingen voor elke taak wordt doorgegeven:
<!-- 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=" -->" />
Deze taak heeft vier thread-veiligheidsproblemen die moeten worden opgelost voor multithreaded-builds:
-
Relatieve paden:
File.ReadAllLinesenFile.WriteAllLinesrechtstreeks gebruikenitem.ItemSpec, wat een relatief pad kan zijn. In de multithreaded-modus is de proceswerkmap niet gegarandeerd de projectmap. -
Statisch veld:
ModifiedFileCountis eenstaticveld dat wordt gedeeld in alle exemplaren, waardoor gegevensraces worden veroorzaakt wanneer meerdere builds gelijktijdig worden uitgevoerd. -
Omgevingsvariabelen: Het meest voorkomende probleem met omgevingsvariabelen in multithreaded builds is dat taken omgevingsvariabelen instellen voordat ze een childproces starten, in de verwachting dat het childproces deze overneemt. In multithreaded modus wijzigt
Environment.SetEnvironmentVariable()de procesomgeving die door alle gelijktijdige builds wordt gedeeld, waardoor een wijziging die bedoeld is voor het subproces van het ene project kan doorwerken in dat van een ander project. Het rechtstreeks lezen van omgevingsvariabelen in taakcode (Environment.GetEnvironmentVariable()) is over het algemeen ook een slechte gewoonte; MSBuild-eigenschappen zijn een beter alternatief omdat ze worden geregistreerd en traceerbaar zijn.
Belangrijk
De multithreaded build-modus is momenteel alleen beschikbaar voor CLI- (dotnet build en MSBuild.exe) builds. Visual Studio MSBuild-builds ondersteunen uitvoering met meerdere threads binnen hetzelfde proces nog niet. In Visual Studio worden alle taken nog steeds buiten het proces uitgevoerd. Visual Studio integratie is gepland voor een toekomstige release.
Prerequisites
MSBuild 18.6 of hoger.
Schakel multithreaded taakuitvoering in met de
-mtopdrachtregelswitch:dotnet build -mt
De migratie plannen
Controleer uw taakcode op de volgende problemen:
- Controleer de taakcode en identificeer het gebruik van relatieve paden. Controleer alle invoer- en bestand-I/O.
- Controleer of er omgevingsvariabelen worden gebruikt.
- Controleer op het gebruik van
ProcessStartInfoAPI's. - Controleer eventuele statische velden of gegevensstructuren en gebruik standaardmethoden om ze thread-veilig te maken.
- Als geen van de bovenstaande opties van toepassing is, kunt u overwegen het kenmerk alleen toe te voegen.
- Overweeg speciale vereisten voor het ondersteunen van eerdere versies van MSBuild. Zie Ondersteuning voor eerdere versies van MSBuild.
Snelzoekgids voor API-vervanging
De volgende tabel bevat een overzicht van de .NET API's die u moet vervangen en de bijbehorende TaskEnvironment equivalenten:
| .NET-API die u moet vermijden | Level | Vervanging |
|---|---|---|
Path.GetFullPath(path) |
ERROR | Zie opmerking na deze tabel |
File.* met relatieve paden |
ERROR | Los dit eerst op met TaskEnvironment.GetAbsolutePath() |
Directory.* met relatieve paden |
ERROR | Los eerst op met 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 | Gebruik ToolTask of TaskEnvironment.GetProcessStartInfo() |
| Statische velden | WAARSCHUWING | Exemplaarvelden of threadveilige verzamelingen gebruiken |
Note
Path.GetFullPath(path) doet twee dingen: het converteert een relatief pad naar een absoluut pad en produceert een canonieke vorm van het pad (omzetten . en .. segmenten). Deze moeten afzonderlijk worden afgehandeld:
-
Alleen absoluut pad: Gebruik
TaskEnvironment.GetAbsolutePath(path). Deze methode is voldoende voor de meeste bestands-I/O-bewerkingen waarbij u het pad rechtstreeks doorgeeft aan .NET API's. -
Canoniek pad: Als u afhankelijk bent van de canonieke vorm (bijvoorbeeld wanneer u een pad gebruikt als sleutel in een cache of woordenlijst), gebruikt u
Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path))om een volledig opgelost, canoniek absoluut pad op te halen.
De taak markeren met het kenmerk
Alle taken die deelnemen aan multithreaded-builds, moeten worden gemarkeerd met het [MSBuildMultiThreadableTask] kenmerk. Dit kenmerk is het signaal dat MSBuild gebruikt om taken te identificeren die veilig zijn om in-process uit te voeren.
[MSBuildMultiThreadableTask]
public class MyTask : Task
{
public override bool Execute()
{
// Task logic that doesn't depend on process-level state
return true;
}
}
Als uw taak al thread-veilig is en geen API's op procesniveau (huidige werkmap, omgevingsvariabelen) gebruikt, ProcessStartInfois het kenmerk alleen alles wat u nodig hebt. De taak blijft overnemen van Task (of ToolTask) zonder andere wijzigingen.
Als uw taak API-aanroepen op procesniveau moet vervangen (bijvoorbeeld om relatieve paden op te lossen of omgevingsvariabelen veilig te lezen), implementeert u IMultiThreadableTaskook . Deze interface geeft uw taak toegang tot de TaskEnvironment eigenschap. Het attribuut blijft in beide gevallen verplicht; IMultiThreadableTask is een extra stap die de TaskEnvironment-API ontgrendelt.
Note
MSBuild detecteert MSBuildMultiThreadableTaskAttribute alleen aan de naamruimte en naam, waarbij de definiërende assembly wordt genegeerd. Dit betekent dat u het kenmerk zelf in uw eigen code kunt definiëren (zie Ondersteuning eerdere versies van MSBuild) en MSBuild herkent het nog steeds.
Note
Het MSBuildMultiThreadableTaskAttribute is niet-overnemend (Inherited = false). Elke taakklasse moet expliciet het kenmerk declareren dat moet worden herkend als multithreadable. Het overnemen van een klasse met het kenmerk maakt de afgeleide klasse niet automatisch multithreadable.
TaskEnvironment initialiseren op fallbackmodus
Bij de implementatie IMultiThreadableTaskinitialiseert u de TaskEnvironment eigenschap in TaskEnvironment.Fallback:
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
MSBuild stelt deze eigenschap in voordat deze wordt aangeroepen Execute() in een normale build. De Fallback standaardinstelling zorgt ervoor dat de taak correct werkt in andere hostingscenario's (zoals eenheidstests of aangepaste buildindelingshulpprogramma's) waarbij MSBuild niet aanwezig is om de eigenschap in te stellen. Zonder dit zou toegang tot TaskEnvironment buiten de engine een null-referentie-uitzondering veroorzaken.
Als u MSBuild-versies moet ondersteunen die ouder zijn dan 18.6 en waarin TaskEnvironment.Fallback niet is opgenomen, initialiseert u de eigenschap in plaats daarvan op null en beschermt u alle aanroepen van TaskEnvironment met een null-controle. Zie Ondersteuning voor eerdere versies van MSBuild voor meer opties.
Padnamen en bestandsinvoer/-uitvoer bijwerken
Een taak accepteert vaak invoerwaarden, zoals itemlijsten in MSBuild, die, wanneer het bestanden betreft, mogelijk in de vorm van relatieve paden zijn.
Relatieve paden zijn altijd relatief ten opzichte van de huidige werkmap van het proces, maar omdat de taak nu in het proces wordt uitgevoerd, is de werkmap mogelijk niet hetzelfde als toen de taak in een eigen proces werd uitgevoerd. Dergelijke paden zijn relatief ten opzichte van de projectmap. De TaskEnvironment bevat een ProjectDirectory eigenschap en een GetAbsolutePath() methode die u kunt gebruiken om relatieve paden om te zetten in absolute paden. U hebt ook toegang tot de FullPath metadatum. U hoeft het ItemSpec relatieve pad niet te gebruiken en deze vervolgens absolutiseren.
Het type AbsolutePath
AbsolutePath is een alleen-lezen-struct in Microsoft.Build.Framework die staat voor een gevalideerd, absoluut bestandspad. Belangrijke leden zijn onder andere:
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);
}
De AbsolutePath constructor valideert dat het opgegeven pad is geroot. U kunt ook een AbsolutePath maken door een relatief pad en een basispad op te geven. Door de impliciete conversie naar string kunt u een AbsolutePath direct doorgeven aan elke API die een string-pad verwacht.
De OriginalValue eigenschap behoudt de oorspronkelijke padtekenreeks zoals deze vóór de resolutie is doorgegeven. Deze eigenschap is handig wanneer u relatieve paden in taakuitvoer of logboekberichten moet bewaren. Een taak die bijvoorbeeld vastlegt welke bestanden zijn verwerkt, kan OriginalValue gebruiken in zijn logberichten, zodat paden in de uitvoer relatief en leesbaar blijven, terwijl voor feitelijke bestands-I/O nog steeds het opgeloste Value (of de impliciete conversie van string) wordt gebruikt.
Gebruik TaskEnvironment.GetAbsolutePath() om itempaden op te lossen:
Before:
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));
After:
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}");
Omgaan met bestandsconflicten in parallelle compilaties
Bestandsconflicten kunnen optreden wanneer meerdere taken parallel worden uitgevoerd en hetzelfde bestand openen. Deze zorg geldt zowel voor het traditionele multiprocesmodel als voor de nieuwere in-process multithreaded modus. In beide gevallen kan hetzelfde bestand gelijktijdig worden geopend wanneer:
- Hetzelfde bestand wordt weergegeven in meerdere subprojectversies (bijvoorbeeld een gedeeld configuratiebestand of een gekoppeld bronbestand).
- Een taak leest van en schrijft naar een bestand dat ook door een ander taakexemplaar wordt verwerkt.
Hulpmethoden zoals File.ReadAllLines en File.WriteAllLines bieden geen expliciete controle over bestandsvergrendeling. Wanneer gelijktijdige toegang mogelijk is, kunt u dit gebruiken FileStream met expliciet delen en vergrendelen:
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);
}
Belangrijke richtlijnen voor bestands-I/O in taken met meerdere threads:
- Gebruik
FileShare.Nonevoor read-modify-write-bewerkingen. Met deze instelling voorkomt u dat een andere taak verouderde inhoud leest terwijl u het bestand bijwerkt. - Vang
IOExceptionop en overweeg het opnieuw te proberen. Wanneer een andere taak of proces een vergrendeling vasthoudt, resulteert een poging om te openen inIOException. Een korte nieuwe poging met uitstel is vaak geschikt. - Voorkom dat vergrendelingen op meerdere bestanden tegelijk worden aangehouden. Als twee taken elk bestand vergrendelen en vervolgens proberen het andere te vergrendelen, krijgt u een impasse. Als u meerdere bestanden moet gebruiken, vergrendelt u deze in een consistente volgorde (bijvoorbeeld gesorteerd op volledig pad).
- Houd sloten zo kort mogelijk. Open het bestand, lees, wijzig, schrijf en sluit het in één bewerking. Houd een bestandsvergrendeling niet vast terwijl u niet-gerelateerd werk uitvoert.
Het voorgaande voorbeeld is één benadering. Zie FileStream-klasse, FileShare-enum en Aanbevolen procedures voor beheerde threading voor algemene richtlijnen over threadveilige bestands-I/O in .NET.
Note
TaskEnvironment zelf is niet threadveilig. Dit is alleen van belang als uw taak intern zelf threads aanmaakt (bijvoorbeeld met Parallel.ForEach of Task.Run). De meeste taken doen dit niet. Ze implementeren Execute() lineair en laten MSBuild parallellisme over taakinstanties heen afhandelen. Als uw taak zijn eigen threads maakt, sla dan waarden uit TaskEnvironment op in lokale variabelen voordat u deze start, in plaats van TaskEnvironment gelijktijdig vanuit meerdere threads te benaderen.
Omgevingsvariabelen bijwerken
Note
Het lezen van omgevingsvariabelen in taakcode is over het algemeen een slechte gewoonte, zelfs in builds met één thread. MSBuild-eigenschappen zijn een beter alternatief: ze zijn expliciet gericht, geregistreerd tijdens de build en traceerbaar in het buildlogboek. Als uw taak momenteel een omgevingsvariabele leest om invoer te ontvangen, kunt u overwegen deze te vervangen door een taakeigenschap. Het project kan nog steeds de waarde afleiden van een omgevingsvariabele: <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.
De richtlijnen in deze sectie zijn bedoeld voor het migreren van bestaande taken die al afhankelijk zijn van omgevingsvariabelen. Als u de mogelijkheid hebt om te herstructureren, geeft u de voorkeur aan eigenschappen en items.
Omgevingsvariabelen instellen voor onderliggende processen
Het meest voorkomende probleem met omgevingsvariabelen in multithreaded builds is een taak die een omgevingsvariabele instelt en vervolgens een childproces start, in de verwachting dat het childproces deze erft. In het multi-processmodel heeft Environment.SetEnvironmentVariable() de omgeving van het workerproces voor dat project veilig aangepast. In de multithreaded-modus wordt het proces gedeeld in alle gelijktijdige builds, zodat een wijziging die is bedoeld voor het onderliggende proces van het ene project, in een andere kan lekken.
Samen TaskEnvironment.SetEnvironmentVariable() gebruiken met TaskEnvironment.GetProcessStartInfo() (zie Api-aanroepen voor Update ProcessStart).
GetProcessStartInfo() geeft een ProcessStartInfo terug die vooraf is gevuld met de werkmap van het project en de geïsoleerde omgevingstabel ervan, inclusief alle variabelen die u met SetEnvironmentVariable() hebt ingesteld, zodat onderliggende processen automatisch de juiste, projectgebonden omgeving overnemen.
Before:
Environment.SetEnvironmentVariable("TOOL_OUTPUT_DIR", outputDir);
var startInfo = new ProcessStartInfo("mytool.exe") { UseShellExecute = false };
Process.Start(startInfo); // inherits the modified process-level environment
After:
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
Omgevingsvariabelen lezen in bestaande taken
Als uw bestaande taak omgevingsvariabelen leest en u niet onmiddellijk kunt herstructureren naar taakeigenschappen, vervangt u door Environment.GetEnvironmentVariable()TaskEnvironment.GetEnvironmentVariable(). Deze methodeaanroep leest uit de projectspecifieke omgevingstabel in plaats van uit de gedeelde procesomgeving, zodat gelijktijdige builds elkaar niet verstoren.
Voor (van BuildCommentTask):
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
After:
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Tip
Wanneer u bestaande code bijwerkt die een omgevingsvariabele leest, kunt u overwegen het patroon te vervangen door een taakeigenschap. Maak public bool DisableComments { get; set; } beschikbaar in de taak en laat het project DisableComments="$(DISABLE_BUILD_COMMENTS)" doorgeven. MSBuild registreert de opgeloste waarde, waardoor deze zichtbaar is in het buildlogboek en veel gemakkelijker te diagnosticeren dan een verborgen omgevingsvariabele die wordt gelezen.
ProcessStart-API-aanroepen bijwerken
Normaal gesproken moet u, als een taak een proces start, gebruiken ToolTask, waarmee alles voor u wordt verwerkt. Als u een taak bijwerkt die ProcessStartInfo rechtstreeks aanroept, gebruikt u TaskEnvironment.GetProcessStartInfo(). Hiermee wordt een ProcessStartInfo geconfigureerde werkmap en de geïsoleerde omgevingstabel van het project geretourneerd. Als u ook omgevingsvariabelen instelt voordat u start, gebruikt u eerst TaskEnvironment.SetEnvironmentVariable(), zoals in de vorige sectie wordt weergegeven.
Before:
var startInfo = new ProcessStartInfo("mytool.exe")
{
WorkingDirectory = ".",
UseShellExecute = false
};
Process.Start(startInfo);
After:
ProcessStartInfo startInfo = TaskEnvironment.GetProcessStartInfo();
startInfo.FileName = "mytool.exe";
startInfo.UseShellExecute = false;
Process.Start(startInfo);
Note
Als uw taak wordt overgenomen van ToolTask, wordt de begingegevens van het proces al voor u verwerkt. U hoeft alleen taken bij te werken die ProcessStartInfo rechtstreeks aanmaken.
Statische velden en gegevensstructuren bijwerken om thread-safe te zijn
Statische velden vereisen een zorgvuldige aanpak bij de migratie naar multithreaded builds. Zelfs in het model voor meerdere processen kan één proces meerdere projecten bouwen, zodat de statische status wordt gedeeld, alleen niet gelijktijdig.
De multithreaded-modus voegt een nieuwe dimensie toe aan dit probleem. Meerdere builds kunnen nu hetzelfde proces delen en taken gelijktijdig uitvoeren (met name met MSBuild Server, dat automatisch wordt ingeschakeld met multithreading). Een statisch veld wordt gedeeld tussen alle taakexemplaren in het proces, niet alleen binnen uw build, maar mogelijk tussen afzonderlijke buildaanroepen die gelijktijdig worden uitgevoerd. Twee ontwikkelaars die bijvoorbeeld tegelijkertijd dotnet build uitvoeren op een buildserver, of twee terminalvensters op dezelfde machine, kunnen dezelfde statische toestand delen, waardoor die builds er tegelijkertijd toegang toe hebben.
In het BuildCommentTask voorbeeld wordt het statische veld ModifiedFileCount gedeeld in alle exemplaren:
Before:
private static int ModifiedFileCount = 0;
// In Execute():
ModifiedFileCount++;
Deze code heeft twee problemen. Ten eerste is de ++ operator niet atomisch. Wanneer meerdere taakexemplaren gelijktijdig worden uitgevoerd, kunnen twee threads dezelfde waarde lezen en beide hetzelfde incrementele resultaat schrijven, waardoor verloren aantallen ontstaan. Ten tweede, omdat het veld statisch is, blijft het behouden tussen builds en wordt het gedeeld tussen gelijktijdige builds in hetzelfde proces.
In de volgende secties ziet u twee benaderingen voor het oplossen van deze problemen, van eenvoudig naar meest correct.
Benadering 1: Gebruik een thread-safe maar procesbrede API
De eenvoudigste oplossing is om de incrementbewerking atomair te maken:
private static int ModifiedFileCount = 0;
// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);
Interlocked.Increment voert de read-increment-write uit als één atomische bewerking, zodat er geen aantallen verloren gaan. Deze aanpak lost het concurrencyprobleem op, maar de teller wordt nog steeds door alle builds binnen het proces gedeeld, inclusief opeenvolgende builds en gelijktijdige builds. Als twee builds gelijktijdig worden uitgevoerd, lopen hun bestandsnummers door elkaar (Build A krijgt #1, #3, #5; Build B krijgt #2, #4, #6). Of deze situatie aanvaardbaar is, hangt ervan af of voor uw taak isolatie per build vereist is. Voor een teller voor sequentiële bestandsnummering zoals ModifiedFileCount leidt delen tussen builds tot correctheidsproblemen; gebruik in plaats daarvan RegisterTaskObject (zie Aanpak 2).
Hier is het threadveilige, maar procesbrede API-equivalent InterlockedIncrement, maar in uw eigen code moet u geschikte threadveilige alternatieven vinden voor API's die niet thread-safe zijn. Als uw taak bijvoorbeeld status opslaat met een Dictionary, kunt u overwegen ConcurrentDictionary<TKey,TValue> te gebruiken.
Benadering 2: RegisterTaskObject voor isolatie binnen het buildbereik
Als uw taak een statische toestand nodig heeft die door subprojecten binnen één build-uitvoering wordt gedeeld, maar van andere gelijktijdige builds is geïsoleerd, gebruik dan IBuildEngine4.RegisterTaskObject met RegisteredTaskObjectLifetime.Build. MSBuild beheert de levensduur van het object, dat wordt gemaakt bij het eerste gebruik en opgeschoond wanneer de build afloopt. Houd er rekening mee dat de geregistreerde objecten threadveilig moeten zijn.
Definieer eerst een eenvoudige thread-veilige tellerklasse:
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
Gebruik vervolgens een helpermethode met dubbele controle om de teller op te halen of te maken:
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();
Met deze benadering krijgt elke build-aanroep zijn eigen FileCounter. Alle subprojecten binnen dezelfde build delen de teller (opeenvolgende nummering), maar een afzonderlijke dotnet build die tegelijkertijd op dezelfde machine wordt uitgevoerd, krijgt een andere teller.
RegisteredTaskObjectLifetime.Build geeft MSBuild aan het object te beperken tot de huidige builduitvoering en het op te ruimen wanneer de build eindigt.
De juiste benadering kiezen
Wanneer u besluit hoe statische status moet worden verwerkt, begint u met deze vraag: zijn deze gegevens veilig om te delen in alle builds die ooit in hetzelfde proces kunnen worden uitgevoerd, inclusief opeenvolgende builds en gelijktijdige builds?
MSBuild-werkprocessen blijven bestaan tussen opeenvolgende aanroepen (hergebruik van knooppunten is standaard ingeschakeld), en een MSBuild-proces kan tijdens zijn levensduur mogelijk worden gebruikt voor meerdere builds van oplossingen, niet alleen binnen één aanroep van dotnet build. Stel niet dat een proces slechts één build verwerkt.
Gebruik deze richtlijnen:
- Behoud het statische veld alleen als de gegevens in de cache veilig kunnen worden benaderd door meerdere threads in verschillende projecten en over meerdere builds heen, zonder dat invalidatie tussen builds nodig is. Een cache met onveranderbare gegevens die eenmaal zijn berekend op basis van invoer die nooit verandert (zoals assemblymetagegevens die tijdens het opstarten worden geladen), kunnen bijvoorbeeld in aanmerking komen.
-
Gebruik
IBuildEngine4.RegisterTaskObjectmetRegisteredTaskObjectLifetime.Buildwanneer de toestand voor elke buildaanroep geïsoleerd moet zijn (bijvoorbeeld tellers, accumulatoren of caches die tussen builds moeten worden gereset of niet mogen lekken tussen gelijktijdige builds). Dit is de voorkeursbenadering voor de meest gedeelde onveranderbare status. -
Gebruik
System.Threadingprimitieven (Interlocked,ConcurrentDictionary,lock,ReaderWriterLockSlim) om eventuele bewaarde statische statusthreads veilig te maken, maar onthoud dat thread-veiligheid alleen geen isolatie op buildniveau biedt. Zie aanbevolen werkwijzen voor beheerde threads.
Tip
In het volledige migratievoorbeeld verderop in dit artikel wordt de RegisterTaskObject benadering gebruikt om isolatie binnen het bereik van de build te demonstreren.
Voorbeeld van volledige migratie
De volgende code toont de volledig gemigreerde AddBuildCommentTask , waarbij alle vijf wijzigingen zijn toegepast:
- Heeft het kenmerk
[MSBuildMultiThreadableTask], waarmee het wordt gemarkeerd voor uitvoering binnen het proces. -
IMultiThreadableTaskImplementeert naast de bestaandeTaskbasisklasse en maakt deTaskEnvironmenteigenschap beschikbaar. - Gebruikt
TaskEnvironment.GetAbsolutePath()voor padomzetting. - Gebruikt
TaskEnvironment.GetEnvironmentVariable()in plaats vanEnvironment.GetEnvironmentVariable(). - Gebruikt
IBuildEngine4.RegisterTaskObjectmetRegisteredTaskObjectLifetime.Buildom de bestandsteller te beperken tot de huidige build-aanroep, ter vervanging van de procesbrede statische teller.
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;
}
}
}
Wat gebeurt er met niet-gemigreerde taken
Taken die niet over het [MSBuildMultiThreadableTask] kenmerk beschikken of die niet implementeren IMultiThreadableTask , blijven werken zonder wijzigingen. MSBuild voert deze taken uit in een dochterondernemingsproces TaskHost , dat dezelfde isolatie op procesniveau biedt als eerdere versies van MSBuild. Deze aanpak is langzamer vanwege de overhead van communicatie tussen processen, maar is volledig compatibel met bestaande taakcode. Migratie is optioneel voor juistheid: niet-gemigreerde taken produceren nog steeds de juiste resultaten, maar de migratie verbetert de buildprestaties.
Ondersteuning voor eerdere versies van MSBuild
Als u uw aangepaste taak bijwerkt en deze vervolgens naar anderen distribueert, ondersteunt uw taak clients die MSBuild 18.6 of hoger gebruiken. Voor ondersteuning van clients in eerdere versies van MSBuild hebt u drie opties.
Optie 1: Verminderde prestaties accepteren
Breng geen wijzigingen aan uw taak aan. MSBuild voert niet-toegeschreven taken uit in een dochterondernemingsproces TaskHost , wat langzamer maar volledig compatibel is. Voor deze optie zijn geen codewijzigingen vereist.
Optie 2: Afzonderlijke implementaties onderhouden
Bouw afzonderlijke taakassembly's voor MSBuild 18.6+ en eerdere versies. De MSBuild 18.6+ versie implementeert IMultiThreadableTask en gebruikt TaskEnvironment. De eerdere versie blijft gebruiken Task met API's op procesniveau.
Optie 3: Compatibiliteitsbrug
Definieer de MSBuildMultiThreadableTaskAttribute zelf in uw taaksamenstelling. Omdat MSBuild het kenmerk alleen detecteert op naamruimte en naam (waarbij de gedefinieerde assembly wordt genegeerd), werkt uw zelf gedefinieerde kenmerk in zowel oude als nieuwe versies van MSBuild:
namespace Microsoft.Build.Framework
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}
Wanneer MSBuild 18.6 of hoger wordt uitgevoerd, herkent MSBuild het kenmerk en wordt de taak in het proces uitgevoerd. Bij uitvoering in eerdere versies negeert MSBuild het onbekende kenmerk en wordt de taak uitgevoerd zoals voorheen.
Met deze optie hebt u geen toegang tot TaskEnvironment, dus u moet alles handmatig afhandelen wat het verwerkt, zoals het converteren van al uw relatieve paden naar absolute paden.
Vergelijking van benaderingen
In de volgende tabel worden de drie benaderingen vergeleken bij het uitvoeren in de multithreaded-modus (-mt). In de niet-multithreaded-modus lopen alle taken buiten het proces, ongeacht de manier waarop ze zijn gemarkeerd.
| Methode | Maintenance | Prestaties (18.6+) | Prestaties (oudere) | Toegang tot TaskEnvironment |
|---|---|---|---|---|
| Afzonderlijke implementaties | High | Volledig in uitvoering | Volledig buiten het proces | Ja (18.6+ versie) |
| Compatibiliteitsbrug | Low | Volledig in proces | Volledig buiten het proces | Nee (kenmerk alleen) |
| Geen wijzigingen | Geen | Sidecar (langzamer) | Volledig buiten het proces | No |