Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
MSBuild 18.6 zavádí možnost paralelního sestavování v rámci stejného procesu. Chcete-li tento režim zapnout, zadejte přepínač příkazového řádku -mt. Předchozí verze nástroje MSBuild podporovaly paralelní sestavení, ale sestavení se prováděla v samostatných procesech. Tato změna má vliv na způsob vytváření úkolů. Zatímco dříve se úlohy spouštěly v samostatném procesu, všechny úlohy s podporou vícevláknů se teď spouštějí ve stejném procesu. Většina logiky se sice nemusí měnit, ale existuje několik konstruktorů na úrovni procesu, které je potřeba zpracovat pečlivěji. Konstrukty na úrovni procesu zahrnují aktuální pracovní adresář, proměnné prostředí a informace o spuštění procesu (ProcessStartInfo).
Pro podporu těchto změn msBuild 18.6 zavádí rozhraní IMultiThreadableTask (v Microsoft.Build.Framework) a třídu TaskEnvironment.
TaskEnvironment obsahuje vlastnost ProjectDirectory a metody, jako jsou GetAbsolutePath(), GetEnvironmentVariable(), SetEnvironmentVariable() a GetProcessStartInfo().
Important
Vícevláknový režim je aktuálně k dispozici jako experimentální funkce; v tuto chvíli se nedoporučuje používat v produkčním prostředí. Aktualizace závislostí knihovny MSBuild tak, aby používala rozhraní API pro vícevláknový režim implicitně brání tomu, aby vaše knihovny běžely ve starších verzích Visual Studio a MSBuild. Doporučujeme uživatelům, kteří novinky zkoušejí mezi prvními, aby vyzkoušeli vícevláknový režim a poskytli zpětnou vazbu. Odešlete problémy v úložišti MSBuild GitHub.
Rozhraní IMultiThreadableTask definuje kontrakt pro úlohy, které mohou běžet v procesu v vícevláknových sestaveních:
// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
TaskEnvironment TaskEnvironment { get; set; }
}
Pokud chcete migrovat úlohu, implementujte IMultiThreadableTask spolu s existující Task základní třídou a zpřístupněte TaskEnvironment vlastnost:
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;
// ...
}
Úlohy, které implementují IMultiThreadableTask , se můžou spouštět v procesu. Všechny takové úlohy musí mít také atribut [MSBuildMultiThreadableTask], což je značka, kterou MSBuild používá k povolení spouštění úlohy v procesu. Před přidáním atributu ověřte, že úloha nemá žádné závislosti na konstruktorech na úrovni procesu, jako je aktuální pracovní adresář nebo prostředí, a že jeho kód je bezpečný pro přístup z více vláken. Věnujte zvláštní pozornost zajištění přístupu ke statickým proměnným zabezpečeným vláknem, protože tyto proměnné jsou sdíleny mezi všemi instancemi úloh a mohou být přístupné nebo upraveny různými instancemi úlohy, které jsou spuštěny také ve stejném procesu.
Příklad úlohy: BuildCommentTask
Následující příklad AddBuildCommentTask se používá v tomto článku k ilustraci procesu migrace. Tento úkol předzálohuje komentář sestavení k textovým souborům. Ve výchozím nastavení zapisuje prostý text; Volitelné vlastnosti CommentPrefix a CommentSuffix umožňují volajícím zabalit komentář v odpovídající syntaxi jazyka (například // pro C#, <!-- a --> pro XML, # pro Python nebo 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;
}
}
}
Soubor projektu může vyvolat tento úkol pro různé typy souborů a předat odpovídající syntaxi komentáře pro každý z nich:
<!-- 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=" -->" />
Tato úloha má čtyři problémy se zabezpečením vláken, které je potřeba řešit u vícevláknových sestavení:
-
Relativní cesty:
File.ReadAllLinesaFile.WriteAllLinespoužívají přímoitem.ItemSpec, která může být relativní cestou. V režimu s více vlákny není zaručeno, že pracovní adresář procesu je adresář projektu. -
Statické pole:
ModifiedFileCountjestaticpole sdílené mezi všemi instancemi, což způsobuje datové závody při souběžném spuštění více sestavení. -
Proměnné prostředí: Nejběžnějším problémem s proměnnou prostředí ve vícevláknových sestaveních jsou úlohy, které nastaví proměnné prostředí před vytvořením podřízeného procesu a očekávají, že je podřízený proces zdědí. Ve vícevláknovém režimu
Environment.SetEnvironmentVariable()upravuje proměnné prostředí na úrovni procesu sdílené všemi paralelními sestaveními, takže změna určená pro podřízený proces jednoho projektu se může promítnout i do podřízeného procesu jiného projektu. Čtení proměnných prostředí přímo v kódu úkolu (Environment.GetEnvironmentVariable()) je také obecně chybný postup; Vlastnosti nástroje MSBuild jsou lepší alternativou, protože jsou protokolované a trasovatelné.
Important
Režim vícevláknového sestavení je aktuálně dostupný jenom pro sestavení rozhraní příkazového řádku (dotnet build a MSBuild.exe). Sestavení MSBuild v aplikaci Visual Studio zatím nepodporují vícevláknové spouštění v rámci procesu. V aplikaci Visual Studio se veškeré spouštění úloh nadále provádí mimo proces. Visual Studio integrace se plánuje pro budoucí verzi.
Prerequisites
MSBuild 18.6 nebo novější.
Povolení provádění vícevláknových úloh pomocí přepínače příkazového
-mtřádku:dotnet build -mtDalší informace o přepínači
-mtnaleznete v referenci k příkazovému řádku nástroje MSBuild.
Plánování migrace
V kódu úkolu zkontrolujte následující problémy:
- Zkontrolujte kód úkolu a identifikujte použití relativních cest. Zkontrolujte všechny vstupy a vstupně-výstupní operace se soubory.
- Zkontrolujte, zda se někde nepoužívají proměnné prostředí.
- Zkontrolujte, zda se používá nějaké
ProcessStartInfoAPI. - Zkontrolujte všechna statická pole nebo datové struktury a použijte standardní metody, aby byly bezpečné pro přístup z více vláken.
- Pokud žádná z výše uvedených možností neplatí, zvažte přidání pouze atributu.
- Zvažte zvláštní požadavky na podporu dřívějších verzí nástroje MSBuild. Viz Podpora starších verzí nástroje MSBuild.
Stručná referenční příručka pro nahrazení rozhraní API
Následující tabulka shrnuje .NET rozhraní API, která byste měli nahradit, a jejich ekvivalenty TaskEnvironment:
| Rozhraní API .NET, kterému je vhodné se vyhnout | Úroveň | Replacement |
|---|---|---|
Path.GetFullPath(path) |
CHYBA | Viz poznámka za touto tabulkou. |
File.* s relativními cestami |
CHYBA | Nejprve vyřešte s TaskEnvironment.GetAbsolutePath() |
Directory.* s relativními cestami |
CHYBA | Nejprve vyřešte s TaskEnvironment.GetAbsolutePath() |
Environment.GetEnvironmentVariable() |
CHYBA | TaskEnvironment.GetEnvironmentVariable() |
Environment.SetEnvironmentVariable() |
CHYBA | TaskEnvironment.SetEnvironmentVariable() |
Environment.CurrentDirectory |
CHYBA | TaskEnvironment.ProjectDirectory |
new ProcessStartInfo() |
CHYBA | TaskEnvironment.GetProcessStartInfo() |
Process.Start() |
CHYBA | Použijte ToolTask nebo TaskEnvironment.GetProcessStartInfo() |
| Statická pole | UPOZORNĚNÍ | Používejte pole instance nebo kolekce bezpečné pro přístup z více vláken |
Note
Path.GetFullPath(path) provádí dvě věci: převádí relativní cestu na absolutní cestu a vytváří kanonickou podobu cesty (vyhodnocením segmentů . a ..). Musí se zpracovávat samostatně:
-
Pouze absolutní cesta: Použijte
TaskEnvironment.GetAbsolutePath(path). Tento přístup je dostačující pro většinu operací se vstupem a výstupem souborů, při nichž předáváte cestu přímo rozhraním API platformy .NET. -
Kanonická cesta: Pokud se spoléháte na kanonickou podobu (například když cestu používáte jako klíč v mezipaměti nebo slovníku), použijte
Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path))k získání plně vyhodnocené kanonické absolutní cesty.
Označení úkolu atributem
Všechny úkoly, které se účastní vícevláknových sestavení, musí být označeny atributem [MSBuildMultiThreadableTask] . Tento atribut je signál, který NÁSTROJ MSBuild používá k identifikaci úloh, které jsou bezpečné ke spuštění v procesu.
[MSBuildMultiThreadableTask]
public class MyTask : Task
{
public override bool Execute()
{
// Task logic that doesn't depend on process-level state
return true;
}
}
Pokud je vaše úloha již bezpečná pro vlákna a nepoužívá žádná API na úrovni procesu (aktuální pracovní adresář, proměnné prostředí, ProcessStartInfo), samotný atribut je vše, co potřebujete. Úkol bude dál dědit z Task (nebo ToolTask) bez jakýchkoli dalších změn.
Pokud vaše úloha skutečně potřebuje nahradit volání API na úrovni procesu (například kvůli bezpečnému řešení relativních cest nebo čtení proměnných prostředí), implementujte také IMultiThreadableTask. Toto rozhraní poskytuje vašemu úkolu přístup k vlastnosti TaskEnvironment. Atribut zůstává v obou případech povinný; IMultiThreadableTask je další krok, který zpřístupní rozhraní TaskEnvironment API.
Note
MSBuild rozpozná MSBuildMultiThreadableTaskAttribute pouze podle oboru názvů a názvu a ignoruje definující sestavení. To znamená, že atribut můžete definovat sami ve svém vlastním kódu (viz Podpora starších verzí NÁSTROJE MSBuild) a NÁSTROJ MSBuild ho stále rozpozná.
Note
Je MSBuildMultiThreadableTaskAttribute neděditelný (Inherited = false). Každá třída úlohy musí explicitně deklarovat atribut, který má být rozpoznán jako multithreadable. Odvození z třídy, která má tento atribut, automaticky neznamená, že je odvozená třída bezpečná pro více vláken.
Inicializovat TaskEnvironment na záložní hodnotu
Při implementaci IMultiThreadableTask inicializujte vlastnost TaskEnvironment na TaskEnvironment.Fallback:
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
Nástroj MSBuild nastaví tuto vlastnost před voláním Execute() v normálním sestavení. Výchozí hodnota Fallback zajišťuje, že úloha funguje správně i v jiných scénářích hostování (například při jednotkových testech nebo ve vlastních nástrojích pro orchestraci sestavení), kde není k dispozici nástroj MSBuild, který by tuto vlastnost nastavil. Bez něj by přístup k TaskEnvironment mimo engine vyvolal výjimku způsobenou odkazem na hodnotu null.
Pokud potřebujete podporovat verze nástroje MSBuild starší než 18.6, které neobsahují TaskEnvironment.Fallback, inicializujte místo toho vlastnost na null a opatřete všechna volání TaskEnvironment kontrolou na hodnotu null. Další možnosti najdete v tématu Podpora starších verzí nástroje MSBuild .
Aktualizace cest a vstupně-výstupních operací souborů
Úkol často přijímá vstupy, například seznamy položek v nástroji MSBuild, které pokud jsou soubory, mohou být ve formě relativních cest.
Relativní cesty jsou vždy vztažené k aktuálnímu pracovnímu adresáři procesu, ale protože se úloha nyní spouští v rámci procesu, pracovní adresář nemusí být stejný jako tehdy, když se úloha spouštěla ve vlastním procesu. Tyto cesty jsou relativní k adresáři projektu.
TaskEnvironment Zahrnuje ProjectDirectory vlastnost a metoduGetAbsolutePath(), kterou můžete použít k překladu relativních cest k absolutním cestám. Máte také přístup k metadatu FullPath; není nutné používat relativní cestu ItemSpec a pak ji převádět na absolutní.
Typ AbsolutePath
AbsolutePath je struktura jen pro čtení v Microsoft.Build.Framework, která představuje ověřenou absolutní cestu k souboru. Mezi klíčové členy patří:
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);
}
Konstruktor AbsolutePath kontroluje, zda zadaná cesta začíná od kořene. Můžete také vytvořit AbsolutePath tak, že poskytnete relativní cestu a základní cestu. Implicitní převod na string znamená, že můžete předat AbsolutePath přímo libovolnému rozhraní API, které očekává cestu typu string.
Vlastnost OriginalValue zachovává původní řetězec cesty v podobě, v jaké byl předán před vyhodnocením. Tato vlastnost je užitečná, když potřebujete zachovat relativní cesty ve výstupech úkolů nebo ve zprávách protokolu. Například úloha, která zaznamenává, které soubory zpracovala, může ve svých protokolových zprávách používat OriginalValue, aby cesty ve výstupu zůstaly relativní a čitelné, a přitom pro skutečné vstupně-výstupní operace se soubory stále používat vyhodnocené Value (nebo implicitní převod na string).
Slouží TaskEnvironment.GetAbsolutePath() k vyřešení cest položek:
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}");
Zpracování kolizí souborů v paralelních buildech
Ke konfliktu při přístupu k souboru může dojít kdykoli, když více úloh běží paralelně a přistupují ke stejnému souboru. Tento problém se týká tradičního modelu s více procesy i novějšího vícevláknového režimu. V obou případech se ke stejnému souboru může přistupovat souběžně, když:
- Stejný soubor se zobrazí v několika sestaveních dílčích projektů (například ve sdíleném konfiguračním souboru nebo propojeném zdrojovém souboru).
- Úloha čte a zapisuje soubor, který zpracovává také jiná instance úlohy.
Metody pohodlí, jako jsou File.ReadAllLines a File.WriteAllLines neposkytují explicitní kontrolu nad zamykáním souborů. Pokud je možný souběžný přístup, použijte FileStream s explicitním sdílením a uzamčením:
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);
}
Klíčové pokyny pro vstupně-výstupní operace souborů v úlohách s více vlákny:
- Slouží
FileShare.Nonepro operace čtení a úpravy zápisu. Toto nastavení brání jinému úkolu v čtení zastaralého obsahu při aktualizaci souboru. - Zachyťte
IOExceptiona zvažte opětovný pokus. Když jiný úkol nebo proces drží zámek, pokus o otevření vyhodíIOException. Krátký opakovaný pokus s prodlevou je často vhodný. - Vyhněte se blokování zámků u více souborů najednou. Pokud dva úkoly každý uzamkne jeden soubor a pak se pokusí uzamknout ten druhý, dojde k uváznutí. Pokud potřebujete pracovat s více soubory, zamkněte je v konzistentním pořadí (například seřazené podle úplné cesty).
- Udržujte dobu uzamčení co nejkratší. Otevřít soubor, přečíst, upravit, zapsat a zavřít v rámci jedné operace. Při provádění nesouvisející práce neudržujte zámek souboru.
Předchozí příklad je jedním z přístupů. Obecné pokyny k vláknově bezpečnému vstupu a výstupu souborů v .NET najdete v tématech třída FileStream, výčet FileShare a osvědčené postupy pro správu vláken.
Note
TaskEnvironment není vláknově bezpečný. Záleží jenom na tom, jestli váš úkol interně vytváří vlastní vlákna (například pomocí Parallel.ForEach nebo Task.Run). Většina úkolů to nedělá. Implementují Execute() lineárně a umožňují MSBuild zpracovávat paralelismus napříč instancemi úloh. Pokud vaše úloha vytváří vlastní vlákna, uložte hodnoty z TaskEnvironment do místních proměnných před jejich spuštěním, namísto souběžného přístupu k TaskEnvironment z více vláken.
Aktualizace proměnných prostředí
Note
Čtení proměnných prostředí v kódu úlohy je obecně chybný postup, a to i v sestaveních s jedním vláknem. Vlastnosti nástroje MSBuild jsou lepší alternativou: jsou explicitně vymezeny, protokolovány během sestavení a trasovatelné v protokolu sestavení. Pokud váš úkol aktuálně čte proměnnou prostředí pro příjem vstupu, zvažte jeho nahrazení vlastností úkolu. Projekt může stále odvodit hodnotu z proměnné prostředí: <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.
Pokyny v této části jsou určené k migraci existujících úloh, které už spoléhají na proměnné prostředí. Pokud máte možnost refaktorovat, upřednostněte vlastnosti a položky.
Nastavení proměnných prostředí pro podřízené procesy
Nejběžnějším problémem s proměnnými prostředí při vícevláknovém sestavování je úloha, která nastaví proměnnou prostředí a potom spustí podřízený proces s očekáváním, že ji tento proces zdědí. V modelu s více procesy Environment.SetEnvironmentVariable() bezpečně upravil prostředí pracovního procesu pro tento projekt. V režimu s více vlákny se proces sdílí napříč všemi souběžnými sestaveními, takže změna určená pro podřízený proces jednoho projektu může uniknout do jiného.
Použijte TaskEnvironment.SetEnvironmentVariable() společně s TaskEnvironment.GetProcessStartInfo() (viz Aktualizace volání rozhraní API ProcessStart).
GetProcessStartInfo() vrátí ProcessStartInfo, předvyplněný pracovním adresářem projektu a jeho izolovanou tabulkou prostředí, včetně všech proměnných, které nastavíte pomocí SetEnvironmentVariable(), takže podřízené procesy automaticky zdědí správné prostředí omezené na daný projekt.
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
Čtení proměnných prostředí v existujících úlohách
Pokud váš stávající úkol čte proměnné prostředí a nemůžete jej okamžitě refaktorovat na vlastnosti úkolu, nahraďte Environment.GetEnvironmentVariable() výrazem TaskEnvironment.GetEnvironmentVariable(). Toto volání metody načítá z tabulky proměnných prostředí v rozsahu projektu, nikoli ze sdílené sady proměnných prostředí procesu, takže se souběžná sestavení vzájemně neovlivňují.
Před (od BuildCommentTask):
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
After:
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Tip
Při aktualizaci existujícího kódu, který čte proměnnou prostředí, zvažte nahrazení tohoto postupu vlastností úlohy. Například zpřístupněte public bool DisableComments { get; set; } v úloze a umožněte projektu předat DisableComments="$(DISABLE_BUILD_COMMENTS)". MSBuild zaznamená vyhodnocenou hodnotu, čímž je viditelná v protokolu sestavení a její diagnostika je mnohem snazší než při čtení skryté proměnné prostředí.
Aktualizujte volání rozhraní API ProcessStart
Obvykle platí, že pokud úkol spustí proces, měli byste použít ToolTask, který zpracovává vše za vás. V případech, kdy aktualizujete úkol, který volá ProcessStartInfo přímo, použijte TaskEnvironment.GetProcessStartInfo(). Tím se vrátí ProcessStartInfo, který je nakonfigurován s pracovním adresářem projektu a jeho tabulkou izolovaného prostředí. Pokud také nastavujete proměnné prostředí před spuštěním, použijte TaskEnvironment.SetEnvironmentVariable() nejprve, jak je znázorněno v předchozí části.
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
Pokud váš úkol dědí z ToolTask, informace o spuštění procesu už jsou zpracovány za vás. Stačí aktualizovat pouze úkoly, které přímo vytvářejí ProcessStartInfo.
Aktualizujte statická pole a datové struktury tak, aby byly bezpečné z hlediska vláken
Statické členy vyžadují pečlivé zvážení při přechodu na vícevláknová sestavení. I v modelu s více procesy může jeden proces sestavit více projektů, takže statický stav se sdílí, jen ne souběžně.
Režim s více vlákny přidá k tomuto problému novou dimenzi. Více sestavení nyní může sdílet stejný proces a spouštět úlohy souběžně (zejména s MSBuild Serverem, který se při vícevláknovém zpracování automaticky povolí). Statické pole se sdílí napříč všemi instancemi úloh v procesu, a to nejen v rámci sestavení, ale potenciálně napříč samostatnými vyvoláním sestavení spuštěnými souběžně. Například dva vývojáři, kteří běží dotnet build současně na buildovém serveru, nebo dvě okna terminálu na stejném počítači, můžou sdílet stejný statický stav a teď k němu tito buildy přistupují současně.
V tomto příkladu BuildCommentTask se statické pole ModifiedFileCount sdílí napříč všemi instancemi:
Before:
private static int ModifiedFileCount = 0;
// In Execute():
ModifiedFileCount++;
Tento kód má dva problémy. Za prvé, ++ operátor není atomický. Při souběžné spuštění více instancí úloh mohou dvě vlákna číst stejnou hodnotu a oba zapisovat stejný výsledek přírůstku, což způsobuje ztracené počty. Za druhé, protože tato statická proměnná přetrvává i mezi sestaveními, je sdílena mezi souběžnými sestaveními v rámci stejného procesu.
Následující části ukazují dva přístupy k tomu, jak tyto problémy vyřešit, od nejjednoduššího po nejsprávnější.
Přístup 1: Použijte API bezpečné pro více vláken, ale pro celý proces
Nejjednodušší opravou je provést inkrementaci atomicky:
private static int ModifiedFileCount = 0;
// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);
Interlocked.Increment provádí čtení-inkrement-zápis jako jednu atomické operace, takže se neztratí žádné počty. Tento přístup řeší problém souběžnosti, ale čítač se stále sdílí napříč všemi sestaveními v procesu, včetně po sobě jdoucích sestavení a souběžných sestavení. Pokud dvě sestavení běží současně, jejich čísla souborů se budou prokládat (sestavení A získá č. 1, 3, 5; sestavení B získá č. 2, 4, 6). To, zda je tato situace přijatelná, závisí na tom, zda váš úkol vyžaduje izolaci pro každé sestavení. U čítače pro sekvenční číslování souborů, jako je ModifiedFileCount, představuje sdílení mezi sestaveními problém z hlediska správnosti; místo něj použijte RegisterTaskObject (viz postup 2).
Zde je vláknově bezpečným, ale pro celý proces platným ekvivalentem rozhraní API InterlockedIncrement, ale ve vlastním kódu byste museli najít vhodné vláknově bezpečné alternativy pro všechna rozhraní API, která nejsou vláknově bezpečná. Pokud například váš úkol uchovává stav pomocí Dictionary, zvažte použití ConcurrentDictionary<TKey,TValue>.
Přístup 2: RegisterTaskObject pro izolaci s vymezeným oborem sestavení
Pokud vaše úloha potřebuje statický stav, který je sdílen napříč dílčími projekty v rámci jednoho spuštění sestavení, ale izolován od jiných souběžných sestavení, použijte IBuildEngine4.RegisterTaskObject s RegisteredTaskObjectLifetime.Build. MSBuild spravuje životní cyklus objektu, který se vytvoří při prvním použití a odstraní se, když sestavení skončí. Upozorňujeme, že registrované objekty musí být vláknově bezpečné.
Nejprve definujte jednoduchou vláknově bezpečnou třídu čítače:
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
Pak pomocí pomocné metody s dvojitou kontrolou uzamčení získejte nebo vytvořte čítač:
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;
}
V Execute():
FileCounter counter = GetOrCreateCounter();
// ...
int fileNumber = counter.Next();
S tímto přístupem získá každé vyvolání sestavení své vlastní FileCounter. Všechny dílčí projekty v rámci stejného sestavení sdílejí čítač (sekvenční číslování), ale samostatný dotnet build spuštěný současně na stejném počítači získá jiný čítač.
RegisteredTaskObjectLifetime.Build určuje nástroji MSBuild, aby omezil platnost objektu na aktuální spuštění sestavení a po jeho dokončení ho odstranil.
Volba správného přístupu
Při rozhodování o tom, jak zpracovat statický stav, začněte z této otázky: Jsou tato data bezpečná ke sdílení napříč všemi sestaveními, která by mohla běžet ve stejném procesu, včetně po sobě jdoucích sestavení a souběžných sestavení?
Pracovní procesy MSBuild se zachovají napříč vyvoláním (ve výchozím nastavení je zapnuté opakované použití uzlu) a proces MSBuild může potenciálně obsluhovat více sestavení řešení po celou dobu jeho životnosti, nejen během jednoho dotnet build volání. Nepředpokládejte, že proces zpracovává pouze jedno sestavení.
Postupujte podle těchto pokynů:
- Statické pole zachovejte pouze v případě, že data uložená v mezipaměti jsou bezpečná pro přístup z více vláken v různých projektech a napříč několika sestaveními, aniž by bylo nutné mezi sestaveními zneplatnit. Může se například kvalifikovat mezipaměť neměnných dat vypočítaných jednou ze vstupů, které se nikdy nemění (například metadata sestavení načtená jednou při spuštění).
-
Použijte
IBuildEngine4.RegisterTaskObjectsRegisteredTaskObjectLifetime.Build, když musí být stav izolován pro každé spuštění sestavení (například u čítačů, akumulátorů nebo mezipamětí, které by se měly mezi sestaveními resetovat nebo se neměly přenášet mezi souběžně probíhajícími sestaveními). Tento přístup je upřednostňovaný pro většinu sdíleného proměnlivého stavu. -
Použijte
System.Threadingprimitiva (Interlocked,ConcurrentDictionary,lock,ReaderWriterLockSlim), aby byl jakýkoli zachovaný statický stav bezpečný pro přístup z více vláken, ale pamatujte, že samotná vláknová bezpečnost nezajišťuje izolaci na úrovni buildu. Viz osvědčené postupy pro spravované vláknování.
Tip
Kompletní příklad migrace dále v tomto článku využívá přístup RegisterTaskObject, aby demonstroval izolaci na úrovni sestavení.
Příklad dokončení migrace
Následující kód zobrazuje plně migrovaný AddBuildCommentTask se všemi pěti použitými změnami:
- Má atribut
[MSBuildMultiThreadableTask], který jej označuje ke spouštění v procesu. - Implementuje
IMultiThreadableTaskspolu s existujícíTaskzákladní třídou a zveřejňujeTaskEnvironmentvlastnost. - Používá
TaskEnvironment.GetAbsolutePath()se k řešení cesty. - Používá
TaskEnvironment.GetEnvironmentVariable()místo .Environment.GetEnvironmentVariable() - Používá
IBuildEngine4.RegisterTaskObjectspolu sRegisteredTaskObjectLifetime.Buildk omezení čítače souborů na aktuální spuštění sestavení, čímž nahrazuje statický čítač platný pro celý proces.
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;
}
}
}
Co se stane s nemigrovanými úlohami
Úkoly, které nemají [MSBuildMultiThreadableTask] atribut nebo neimplementují IMultiThreadableTask , nadále fungují bez jakýchkoli změn. Nástroj MSBuild spouští tyto úlohy v podřízeném TaskHost procesu, který poskytuje stejnou izolaci na úrovni procesu jako starší verze nástroje MSBuild. Tento přístup je pomalejší kvůli režii komunikace mezi procesy, ale je plně kompatibilní s existujícím kódem úlohy. Z hlediska správnosti není migrace nutná – úlohy, které nebyly migrovány, stále poskytují správné výsledky, ale migrace zlepšuje výkon sestavování.
Podpora starších verzí nástroje MSBuild
Pokud aktualizujete vlastní úlohu a pak ji distribuujete ostatním, vaše úloha podporuje klienty pomocí nástroje MSBuild 18.6 nebo novějšího. Pokud chcete podporovat klienty ve starších verzích nástroje MSBuild, máte tři možnosti.
Možnost 1: Přijetí sníženého výkonu
Neprovádějte žádné změny ve svém úkolu. MSBuild spouští úlohy bez atributů v podřízeném procesu TaskHost, což je pomalejší, ale plně kompatibilní. Tato možnost nevyžaduje žádné změny kódu.
Možnost 2: Zachování samostatných implementací
Sestavte samostatná sestavení úloh pro MSBuild 18.6+ a starší verze. Verze MSBuild 18.6+ implementuje IMultiThreadableTask a používá TaskEnvironment. Předchozí verze nadále používá Task s rozhraními API na úrovni procesu.
Možnost 3: Můstek kompatibility
Definujte MSBuildMultiThreadableTaskAttribute sami v sestavení úlohy. Vzhledem k tomu, že nástroj MSBuild rozpozná atribut pouze podle oboru názvů a názvu (ignoruje definující sestavení), funguje váš atribut v starých i nových verzích nástroje MSBuild:
namespace Microsoft.Build.Framework
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}
Při spuštění na MSBuild 18.6 nebo novějším nástroj MSBuild rozpozná atribut a spustí úlohu v procesu. Při spuštění na starších verzích nástroj MSBuild ignoruje neznámý atribut a spustí úlohu jako předtím.
Díky této možnosti nemáte přístup k TaskEnvironment, takže budete muset ručně zpracovat vše, co zpracovává, například převádět všechny relativní cesty na absolutní cesty.
Porovnání přístupů
Následující tabulka porovnává tři přístupy při spuštění v režimu s více vlákny (-mt). V jednovláknovém režimu všechny úlohy běží mimo proces bez ohledu na to, jak jsou označeny.
| Přístup | Maintenance | Výkon (18,6+) | Výkon (starší) | Přístup k taskEnvironmentu |
|---|---|---|---|---|
| Samostatné implementace | Vysoká | Plně probíhající | Plně v samostatném procesu | Ano (verze 18.6 nebo novější) |
| Můstek kompatibility | Nízká | Plně v procesu | Plně mimo proces | Ne (pouze atribut) |
| Žádné změny | None | Sidecar (pomalejší) | Plně mimo proces | No |