Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Az MSBuild 18.6 lehetővé teszi a párhuzamos buildelést ugyanazon a folyamaton belül. Ha ezt a módot szeretné választani, adja át a -mt parancssori kapcsolót. Az MSBuild korábbi verziói támogatták a párhuzamos buildeket, de a buildek külön folyamatokban készültek. Ez a módosítás hatással van arra, hogyan hozza létre a feladatokat. Míg korábban a tevékenységek külön folyamatban futottak, most minden többszálú tevékenység ugyanabban a folyamatban fut. Bár a legtöbb logikának nem kell változnia, vannak olyan folyamatszintű szerkezetek, amelyeket körültekintően kell kezelni. A folyamatszintű szerkezetek közé tartozik az aktuális munkakönyvtár, a környezeti változók és a folyamatindítási információk (ProcessStartInfo).
A módosítások támogatásához az MSBuild 18.6 bevezeti a IMultiThreadableTask felületet (Microsoft.Build.Framework) és a TaskEnvironment osztályt.
TaskEnvironment tartalmaz egy tulajdonságot ProjectDirectory és metódust, például GetAbsolutePath(), GetEnvironmentVariable(), SetEnvironmentVariable()és GetProcessStartInfo().
Important
A többszálas mód jelenleg kísérleti funkcióként érhető el; jelenleg nem ajánlott éles használatra. Az MSBuild-kódtár függőségeinek frissítése a többszálú módú API-k használatára implicit módon megakadályozza, hogy a kódtárak a Visual Studio és az MSBuild régebbi verzióin fussanak. Javasoljuk a korai alkalmazókat, hogy próbáljanak ki többszálú módot, és adjanak visszajelzést. Problémák elküldése a MSBuild GitHub adattárban.
Az IMultiThreadableTask interfész határozza meg a többszálú buildekben folyamatban futtatható feladatokra vonatkozó szerződést:
// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
TaskEnvironment TaskEnvironment { get; set; }
}
Egy feladat migrálásához implementálja a(z) IMultiThreadableTask elemet a meglévő Task alaposztály mellett, és tegye elérhetővé a(z) TaskEnvironment tulajdonságot:
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;
// ...
}
A implementálható IMultiThreadableTask feladatok folyamat közben is futtathatók. Minden ilyen feladatnak rendelkeznie kell a [MSBuildMultiThreadableTask] attribútummal is, amelyet az MSBuild jelölőként használ a feladat folyamaton belüli végrehajtásának engedélyezésére. Az attribútum hozzáadása előtt győződjön meg arról, hogy a tevékenység nem függ a folyamatszintű szerkezetekhez, például az aktuális munkakönyvtárhoz vagy a környezethez, és hogy a kód szálbiztos. Különös figyelmet kell fordítani a statikus változók szálbiztos elérésének biztosítására, mivel ezek a változók meg vannak osztva az összes tevékenységpéldány között, és a feladat különböző példányai is elérhetik vagy módosíthatják őket, amelyek szintén ugyanabban a folyamatban futnak.
Példafeladat: BuildCommentTask
A cikk az alábbi példát AddBuildCommentTask használja a migrálási folyamat szemléltetésére. Ez a feladat előre felvesz egy összeállítási megjegyzést a szövegfájlokra. Alapértelmezés szerint egyszerű szöveget ír; az opcionális CommentPrefix és CommentSuffix tulajdonságok lehetővé teszik a hívók számára, hogy a megjegyzést nyelvnek megfelelő szintaxisba csomagolják (például // c#, <!-- és --> XML, # Python vagy YAML esetén):
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;
}
}
}
Előfordulhat, hogy egy projektfájl meghívja ezt a feladatot különböző fájltípusokhoz, és mindegyikhez átadja a megfelelő megjegyzésszintaxist:
<!-- 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=" -->" />
Ez a feladat négy szálbiztonsági problémával rendelkezik, amelyeket többszálas buildekhez kell kezelni:
-
Relatív elérési utak:
File.ReadAllLinesésFile.WriteAllLinesközvetlenül használhatókitem.ItemSpec, amelyek relatív elérési utak lehetnek. Többszálú módban a folyamat munkakönyvtára nem garantáltan a projektkönyvtár. -
Statikus mező:
ModifiedFileCountazstaticösszes példányon megosztott mező, amely adatversenyeket okoz, ha több build fut egyszerre. -
Környezeti változók: A többszálú buildek leggyakoribb környezeti változóval kapcsolatos problémája az a feladat, amely környezeti változókat állít be a gyermekfolyamat létrehozása előtt, és azt várja, hogy a gyermek örökölje őket. Többszálú módban
Environment.SetEnvironmentVariable()módosítja az összes egyidejű build által megosztott folyamatszintű környezetet, így az egyik projekt gyermekfolyamatához tartozó módosítás a másikéba is kivérezhet. A környezeti változók közvetlenül a feladatkódban (Environment.GetEnvironmentVariable()) való olvasása szintén általában rossz gyakorlat; Az MSBuild tulajdonságok jobb alternatívát jelentenek, mivel naplózhatók és nyomon követhetők.
Important
A többszálú buildelési mód jelenleg csak parancssori felületi (dotnet build és MSBuild.exe) buildekhez érhető el. Visual Studio MSBuild buildek még nem támogatják a többszálú végrehajtást a folyamatban. A Visual Studióban minden feladat végrehajtása továbbra is külön folyamatban történik. Visual Studio integrációt egy későbbi kiadásra tervezik.
Prerequisites
MSBuild 18.6 vagy újabb verzió.
Engedélyezze a többszálú feladat végrehajtását a
-mtparancssori kapcsolóval:dotnet build -mtA kapcsolóval kapcsolatos további információkért tekintse meg az
-mtMSBuild parancssori referenciát.
Az áttelepítés tervezése
Tekintse át a feladatkódot a következő problémák esetén:
- Ellenőrizze a feladatkódot, és azonosítsa a relatív elérési utak használatát. Ellenőrizze az összes bemenetet és fájl I/O-t.
- Ellenőrizze, hogy használ-e környezeti változókat.
- Ellenőrizze, hogy van-e
ProcessStartInfoAPI-használat. - Ellenőrizze az összes statikus mezőt vagy adatstruktúrát, és szabványos módszerekkel tegye biztonságossá a szálakat.
- Ha a fentiek egyike sem érvényes, fontolja meg csak az attribútum hozzáadását.
- Fontolja meg az MSBuild korábbi verzióinak támogatásával kapcsolatos speciális követelményeket. Lásd: Az MSBuild korábbi verzióinak támogatása.
API-csere – rövid útmutató
Az alábbi táblázat összefoglalja a lecserélni kívánt .NET API-kat és azok TaskEnvironment megfelelőit:
| .NET API elkerülése | szint | Replacement |
|---|---|---|
Path.GetFullPath(path) |
ERROR | Lásd a táblázatot követő megjegyzést |
File.* relatív elérési utakkal együtt |
ERROR | Először oldja meg ezzel: TaskEnvironment.GetAbsolutePath() |
Directory.* relatív elérési utakkal |
ERROR | Először oldja meg ezzel: 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 | Használja a ToolTask vagy TaskEnvironment.GetProcessStartInfo() formátumot |
| Statikus mezők | FIGYELMEZTETÉS | Használjon példánymezőket vagy szálbiztos gyűjteményeket |
Megjegyzés:
Path.GetFullPath(path) két dolgot tesz: a relatív elérési utat abszolút elérési úttá alakítja, és előállítja az elérési út kanonikus alakját (feloldva a . és .. szegmenseket). Ezeket külön kell kezelni:
-
Csak abszolút elérési út: Használja
TaskEnvironment.GetAbsolutePath(path). Ez a módszer elegendő a legtöbb olyan fájl I/O-művelethez, ahol közvetlenül .NET API-knak adja át az elérési utat. -
Canonical path: Ha a kanonikus alakra támaszkodik (például ha egy elérési utat gyorsítótár- vagy szótárkulcsként használ), használja a(z)
Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path))elemet egy teljesen feloldott, kanonikus abszolút elérési út lekéréséhez.
A tevékenység megjelölése az attribútummal
A többszálú buildekben részt vevő összes feladatot meg kell jelölni az [MSBuildMultiThreadableTask] attribútummal. Ez az attribútum az a jel, amelyet az MSBuild a folyamatokban biztonságosan futtatható feladatok azonosítására használ.
[MSBuildMultiThreadableTask]
public class MyTask : Task
{
public override bool Execute()
{
// Task logic that doesn't depend on process-level state
return true;
}
}
Ha a feladat már szálbiztos, és nem használ folyamatszintű API-kat (jelenlegi munkakönyvtár, környezeti változók), ProcessStartInfocsak az attribútumra van szüksége. A feladat továbbra is a(z) Task-ból (vagy a(z) ToolTask-ből) öröklődik, minden egyéb módosítás nélkül.
Ha a feladatnak le kell cserélnie a folyamatszintű API-hívásokat (például a relatív útvonalak feloldásához vagy a környezeti változók biztonságos olvasásához), akkor is implementálnia IMultiThreadableTaskkell. Ez a felület hozzáférést biztosít a feladatnak a TaskEnvironment tulajdonsághoz. Az attribútum mindkét esetben kötelező marad; IMultiThreadableTask egy további lépés, amely feloldja a TaskEnvironment API.
Megjegyzés:
Az MSBuild a MSBuildMultiThreadableTaskAttribute elemet csak a névtér és a név alapján azonosítja, a definiáló assembly figyelmen kívül hagyásával. Ez azt jelenti, hogy az attribútumot saját kódjában határozhatja meg (lásd: Az MSBuild korábbi verzióinak támogatása), és az MSBuild továbbra is felismeri azt.
Megjegyzés:
Ez MSBuildMultiThreadableTaskAttribute nem örökölhető (Inherited = false). Minden tevékenységosztálynak explicit módon deklarálnia kell az attribútumot, hogy többszálúként legyen felismerve. Az attribútummal rendelkező osztály öröklése nem teszi automatikusan többszálúvá a származtatott osztályt.
A TaskEnvironment inicializálása tartalék módra
A(z) IMultiThreadableTask megvalósításakor állítsa a(z) TaskEnvironment tulajdonságot TaskEnvironment.Fallback értékre:
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
Az MSBuild beállítja ezt a tulajdonságot, mielőtt normál buildben hívna Execute() meg. Az Fallback alapértelmezett beállítás biztosítja, hogy a feladat megfelelően működjön más üzemeltetési forgatókönyvekben (például egységtesztekben vagy egyéni összeállítási vezénylési eszközökben), ahol az MSBuild nem jelenik meg a tulajdonság beállításához. Nélküle a TaskEnvironment motoron kívüli elérése nullreferencia-kivételt eredményezne.
Ha az 18.6 előtti MSBuild-verziókat is támogatnia kell, amelyek nem tartalmazzák a(z) TaskEnvironment.Fallback elemet, inicializálja helyette a tulajdonságot erre: null, és a(z) TaskEnvironment hívásokat nullérték-ellenőrzéssel védje. További lehetőségekért tekintse meg az MSBuild korábbi verzióinak támogatását .
Elérési utak és fájl I/O frissítése
A tevékenységek gyakran fogadnak bemeneteket, például az MSBuild elemlistáit, amelyek ha fájlok, relatív elérési utak formájában lehetnek.
A relatív elérési utak mindig a folyamat aktuális munkakönyvtárához viszonyítva értendők, de mivel a feladat végrehajtása most a folyamaton belül történik, előfordulhat, hogy a munkakönyvtár nem ugyanaz, mint amikor a feladat a saját folyamatában futott. Az ilyen útvonalak a projektkönyvtárhoz relatívak. A(z) TaskEnvironment rendelkezik egy ProjectDirectory tulajdonsággal és egy GetAbsolutePath() metódussal, amelyekkel a relatív elérési utakat abszolút elérési utakká alakíthatja. A metaadatot a FullPath használatával is elérheti; nincs szükség a ItemSpec relatív elérési út használatára, majd annak abszolút elérési úttá alakítására.
Az AbsolutePath típusa
A(z) AbsolutePath egy a(z) Microsoft.Build.Framework névtérben található, csak olvasható struktúra, amely egy ellenőrzött abszolút fájlelérési utat reprezentál. A fő tagok a következők:
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);
}
A AbsolutePath konstruktor ellenőrzi, hogy a megadott elérési út gyökérútvonallal rendelkezik-e. Létrehozhat egy AbsolutePath elemet egy relatív elérési út és egy alapútvonal megadásával. Az string-ra történő implicit átalakítás azt jelenti, hogy egy AbsolutePath közvetlenül átadható bármely olyan API-nak, amely string elérési utat vár.
A OriginalValue tulajdonság az eredeti elérésiút-karakterláncot abban a formában őrzi meg, ahogyan azt a feloldás előtt átadták. Ez a tulajdonság akkor hasznos, ha relatív elérési utakat kell tartania a tevékenységkimenetekben vagy a naplóüzenetekben. Például egy olyan feladat, amely naplózza, hogy mely fájlokat dolgozza fel, használhatja OriginalValue a naplóüzeneteiben, hogy a kimenet elérési útjai relatívak és olvashatóak maradjanak, miközben a feloldott Value (vagy implicit string konverziót) továbbra is a tényleges fájl I/O-hoz használja.
Az elemek elérési útjainak feloldásához használja a(z) TaskEnvironment.GetAbsolutePath() elemet:
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}");
Fájl versengés kezelése párhuzamos buildekben
A fájl versengése akkor fordulhat elő, ha több feladat fut párhuzamosan, és ugyanazt a fájlt éri el. Ez a probléma a hagyományos többfolyamatos modellre és az újabb, többszálú üzemmódra is vonatkozik. Mindkét esetben ugyanaz a fájl egyidejűleg érhető el, ha:
- Ugyanaz a fájl több alprojekt-buildben (például megosztott konfigurációs fájlban vagy csatolt forrásfájlban) jelenik meg.
- Egy feladat beolvassa és megírja a fájlt, amelyet egy másik feladatpéldány is feldolgoz.
Az olyan kényelmi módszerek, mint a File.ReadAllLines és a File.WriteAllLines, nem biztosítanak kifejezett szabályozást a fájlzárolás felett. Ha egyidejű hozzáférés lehetséges, használja FileStream explicit megosztással és zárolással:
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);
}
A fájl I/O-jának főbb irányelvei többszálú feladatok esetén:
- Olvasási-módosítási-írási műveletekhez használható
FileShare.None. Ez a beállítás megakadályozza, hogy egy másik feladat elavult tartalmat olvasson a fájl frissítése közben. - Kezelje a
IOExceptionkivételt, és fontolja meg az újbóli próbálkozást. Ha egy másik tevékenység vagy folyamat zárolást tartalmaz, a nyitott kísérlet eldobjaIOException. A rövid, késleltetéssel történő újrapróbálás gyakran célszerű. - Ne zároljon egyszerre több fájlt. Ha két feladat zárolja az egyik fájlt, majd megpróbálja zárolni a másikat, holtpontot kap. Ha több fájlon kell dolgoznia, zárolja őket konzisztens sorrendben (például teljes elérési út szerint rendezve).
- Tartsa a zárakat a lehető legrövidebbre. Nyissa meg a fájlt, olvassa el, módosítsa, írja és zárja be egy művelettel. Ne tartsa lenyomva a fájlzárolást, miközben nem kapcsolódó munkát végez.
Az előző példa egy megközelítés. A .NET szálbiztos fájljának I/O-jára vonatkozó általános útmutatásért lásd: FileStream osztály, FileShare enum és A szálkezelés ajánlott eljárásai.
Megjegyzés:
TaskEnvironment önmagában nem szálbiztos. Ez csak akkor számít, ha a feladat belsőleg saját szálakat hoz létre (például használ Parallel.ForEach vagy Task.Run). A legtöbb feladat nem ezt teszi. Lineárisan implementálják Execute() , és lehetővé teszik az MSBuild számára a párhuzamosság kezelését a feladatpéldányok között. Ha a feladat saját szálakat hoz létre, a szálak elindítása előtt mentse ki a TaskEnvironment értékeit helyi változókba, ahelyett, hogy a TaskEnvironment elemhez egyszerre több szálból férne hozzá.
Környezeti változók frissítése
Megjegyzés:
A környezeti változók a feladatkódban való olvasása általában rossz gyakorlat, még az egyszálas buildekben is. Az MSBuild tulajdonságok jobb alternatívát jelentenek: explicit hatókörrel rendelkeznek, a build során naplózhatók, és nyomon követhetők a buildnaplóban. Ha a tevékenység jelenleg egy környezeti változót olvas be a bemenet fogadásához, érdemes lehet inkább egy tevékenységtulajdonságra cserélni. A projekt továbbra is származtathatja az értéket egy környezeti változóból: <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.
Ebben a szakaszban az útmutató a környezeti változókra támaszkodó meglévő tevékenységek migrálására szolgál. Ha van lehetősége refaktorálásra, részesítse előnyben a tulajdonságokat és az elemeket.
Környezeti változók beállítása gyermekfolyamatokhoz
A többszálú buildek leggyakoribb környezeti változóval kapcsolatos problémája egy olyan feladat, amely beállít egy környezeti változót, majd létrehoz egy gyermekfolyamatot, és azt várja, hogy a gyermek örökölje azt. A többfolyamatos modellben Environment.SetEnvironmentVariable() biztonságosan módosította a projekt feldolgozói folyamatkörnyezetét. Többszálas módban a folyamat az összes egyidejű buildben meg van osztva, így az egyik projekt gyermekfolyamatára vonatkozó módosítás kiszivároghat egy másikba.
Használja a(z) TaskEnvironment.SetEnvironmentVariable() elemet a(z) TaskEnvironment.GetProcessStartInfo() elemmel együtt (lásd: A ProcessStart API-hívások frissítése). A GetProcessStartInfo() egy, a projekt munkakönyvtárával és elkülönített környezettáblájával eleve feltöltött ProcessStartInfo-et ad vissza, beleértve az összes változót, amelyet a SetEnvironmentVariable() segítségével állított be, így a gyermekfolyamatok automatikusan a megfelelő, projektszintű környezetet öröklik.
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
Környezeti változók olvasása meglévő tevékenységekben
Ha a meglévő feladat környezeti változókat olvas, és nem tudja azonnal feladattulajdonságokra átalakítani, cserélje le a(z) Environment.GetEnvironmentVariable() elemet erre: TaskEnvironment.GetEnvironmentVariable(). Ez a metódushívás a megosztott folyamatkörnyezet helyett a projekt hatókörű környezeti táblából olvas be, így az egyidejű buildek nem zavarják egymást.
Előző (ettől: BuildCommentTask):
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
After:
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Jótanács
Meglévő, környezeti változót olvasó kód frissítésekor érdemes megfontolni a mintázat feladattulajdonságra történő cseréjét. Tegyük fel például, hogy elérhetővé teszi public bool DisableComments { get; set; } a feladatot, és hagyja, hogy a projekt áthaladjon DisableComments="$(DISABLE_BUILD_COMMENTS)". Az MSBuild naplózza a feloldott értéket, így láthatóvá válik a buildnaplóban, és sokkal könnyebben diagnosztizálható, mint egy rejtett környezeti változó.
ProcessStart API-hívások frissítése
Általában, ha egy feladat elindít egy folyamatot, akkor érdemes használnia ToolTask, amely mindent kezel. Azokban az esetekben, amikor egy olyan feladatot frissít, amely közvetlenül a(z) ProcessStartInfo elemet hívja meg, használja a(z) TaskEnvironment.GetProcessStartInfo() elemet. Ez a projekt munkakönyvtárával és elkülönített környezeti táblájával konfigurált ProcessStartInfo elemet ad vissza. Ha az indítás előtt környezeti változókat is beállít, először használja TaskEnvironment.SetEnvironmentVariable() az előző szakaszban látható módon.
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);
Megjegyzés:
Ha a feladat a(z) ToolTask elemből származik, a folyamat indítási információinak kezelése már meg van oldva Ön helyett. Csak azokat a feladatokat kell frissítenie, amelyek közvetlenül hozzák létre a(z) ProcessStartInfo elemet.
Statikus mezők és adatstruktúrák frissítése szálbiztosságra
A statikus mezők gondos kezelést igényelnek, amikor többszálú buildekre migrál. Még a többfolyamatos modellben is egyetlen folyamat több projektet is létrehozhat, így a statikus állapot meg van osztva, csak nem egyszerre.
A többszálas mód új dimenziót ad ehhez a problémához. Több build is megoszthatja ugyanazt a folyamatot, és párhuzamosan futtathatja a feladatokat (különösen az MSBuild Server esetén, amely automatikusan engedélyezve van a többszálas feldolgozással). A statikus mező a folyamat összes tevékenységpéldánya között meg van osztva, nem csak a builden belül, hanem az egyidejűleg futó különálló buildhívások között is. Előfordulhat például, hogy egy buildkiszolgálón egyszerre futó dotnet build két fejlesztő vagy ugyanazon a gépen két terminálablak ugyanazt a statikus állapotot használja, és most a buildek egyszerre férnek hozzá.
A(z) BuildCommentTask példában a(z) ModifiedFileCount statikus mező az összes példány között meg van osztva:
Before:
private static int ModifiedFileCount = 0;
// In Execute():
ModifiedFileCount++;
Ennek a kódnak két problémája van. Először is, az ++ operátor nem atomi. Ha egyszerre több feladatpéldány is fut, két szál is beolvassa ugyanazt az értéket, és mindkettő ugyanazt a növekményes eredményt írja, ami elveszett számokat okoz. Másodszor, mivel a mező statikus, több buildben is megmarad, és ugyanazon folyamat egyidejű buildjei között van megosztva.
Az alábbi szakaszok két módszert mutatnak be a problémák megoldására a legegyszerűbbtől a legkorrektebbig.
1. megközelítés: Szálbiztos, de folyamatszintű API használata
A legegyszerűbb megoldás az, ha atomivá tesszük a növelést:
private static int ModifiedFileCount = 0;
// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);
Interlocked.Increment Egyetlen atomi műveletként hajtja végre az olvasási növekményes írást, így nem vesznek el a számok. Ez a megközelítés megoldja az egyidejűségi problémát, de a számláló továbbra is meg van osztva a folyamat összes buildjében, beleértve az egymást követő buildeket és az egyidejű buildeket is. Ha két build egyidejűleg fut, a fájlszámuk egybefonódik (az A build #1, #3, #5; A B build #2, #4, #6) lesz. Az, hogy ez a helyzet elfogadható-e, attól függ, hogy a feladat buildenkénti elkülönítést igényel-e. Az olyan szekvenciális fájlszámozási számlálók esetében, mint a ModifiedFileCount, a buildek közötti megosztás helyességi problémát okoz; helyette inkább a RegisterTaskObject használja (lásd a 2. módszert).
Itt ennek a teljes folyamatra kiterjedő, szálbiztos API-megfelelője a InterlockedIncrement, de a saját kódjában meg kell találnia a megfelelő szálbiztos helyettesítőket azokhoz az API-khoz, amelyek nem szálbiztosak. Ha például a feladata egy Dictionary használatával megőrzi az állapotot, fontolja meg a(z) ConcurrentDictionary<TKey,TValue> használatát.
2. megközelítés: RegisterTaskObject buildszintű elkülönítéshez
Ha a feladatnak olyan statikus állapotra van szüksége, amely egyetlen buildhíváson belül, de más egyidejű buildektől elkülönítve van megosztva az alprojektekben, használja IBuildEngine4.RegisterTaskObject a következővel RegisteredTaskObjectLifetime.Build: . Az MSBuild kezeli az objektum élettartamát, amely az első használatkor jön létre, és a build befejezésekor törlődik. Vegye figyelembe, hogy a regisztrált objektumoknak szálbiztosnak kell lenniük.
Először definiáljon egy egyszerű szálbiztos számlálóosztályt:
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
Ezután használjon egy segédmetódust duplán ellenőrzött zárolással a számláló lekéréséhez vagy létrehozásához:
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();
Ezzel a megközelítéssel minden buildfuttatás saját FileCounter-t kap. Az ugyanazon a builden belüli összes alprojekt ugyanazt a számlálót használja (szekvenciális számozás), de egy különálló, ugyanazon a gépen egyidejűleg futó dotnet build másik számlálót kap.
RegisteredTaskObjectLifetime.Build azt jelzi az MSBuild számára, hogy az objektum hatókörét az aktuális buildfuttatásra korlátozza, és a build végén szabadítsa fel.
Válassza ki a megfelelő megközelítést
A statikus állapot kezelésének eldöntésekor kezdje ebből a kérdésből: biztonságos-e ezeket az adatokat megosztani minden olyan buildben, amely valaha is ugyanabban a folyamatban fut, beleértve az egymást követő buildeket és az egyidejű buildeket?
Az MSBuild feldolgozói folyamatok a meghívások során is megmaradnak (a csomópontok újrafelhasználása alapértelmezés szerint be van kapcsolva), és az MSBuild folyamat akár több megoldás buildjét is kiszolgálhatja az élettartama során, nem csak egyetlen dotnet build híváson belül. Ne feltételezzük, hogy egy folyamat csak egy buildet kezel.
Kövesse az alábbi irányelveket:
- A statikus mezőt csak akkor őrizze meg , ha a gyorsítótárazott adatok biztonságosan elérhetők több szálról különböző projektekben és több buildben anélkül, hogy érvényteleníteni kellene a buildek között. Például megfelelhet egy olyan, egyszer kiszámított immutábilis adatokból álló gyorsítótár, amely olyan bemenetek alapján készül, amelyek soha nem változnak (például az indításkor egyszer betöltött assemblymetaadatok).
-
IBuildEngine4.RegisterTaskObjectAkkor használhatóRegisteredTaskObjectLifetime.Build, ha az állapotot buildhívásonként el kell különíteni (például számlálók, akkumulátorok vagy gyorsítótárak, amelyeknek alaphelyzetbe kell állítaniuk a buildek között, vagy nem szivárognak ki az egyidejű buildek között). A legtöbb megosztott, módosítható állapot esetében ez az előnyben részesített megközelítés. -
Használjon
System.Threadingprimitíveket (Interlocked,ConcurrentDictionary,lock,ReaderWriterLockSlim) a megtartott statikus állapotok biztonságossá tétele érdekében, de ne feledje, hogy a szálbiztonság önmagában nem biztosít buildszintű elkülönítést. Tekintse meg a felügyelt szálkezelés ajánlott eljárásait.
Jótanács
A cikk későbbi részében szereplő teljes migrálási példa a buildre kiterjedő elkülönítés bemutatására használja a RegisterTaskObject megközelítést.
Teljes migrálási példa
Az alábbi kód a teljes migrálást AddBuildCommentTask mutatja mind az öt módosítással:
- Rendelkezik a(z)
[MSBuildMultiThreadableTask]attribútummal, amely folyamaton belüli végrehajtásra jelöli. - Megvalósítja a(z)
IMultiThreadableTaskelemet a meglévőTaskalaposztály mellett, és elérhetővé teszi a(z)TaskEnvironmenttulajdonságot. - Az elérési utak feloldásához a
TaskEnvironment.GetAbsolutePath()elemet használja. -
TaskEnvironment.GetEnvironmentVariable()-t használEnvironment.GetEnvironmentVariable()helyett. -
IBuildEngine4.RegisterTaskObjectésRegisteredTaskObjectLifetime.Buildhasználatával a fájlszámláló hatókörét az aktuális build meghívására korlátozza, lecserélve a folyamat egészére kiterjedő statikus számlálót.
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;
}
}
}
Mi történik a nem migrált feladatokkal?
Azok a tevékenységek, amelyek nem rendelkeznek az [MSBuildMultiThreadableTask] attribútummal, vagy nem implementálják IMultiThreadableTask , módosítások nélkül működnek tovább. Az MSBuild ezeket a feladatokat egy másodlagos TaskHost folyamatban futtatja, amely ugyanazt a folyamatszintű elkülönítést biztosítja, mint az MSBuild korábbi verziói. Ez a megközelítés lassabb a folyamatközi kommunikáció többletterhelése miatt, de teljes mértékben kompatibilis a meglévő feladatkóddal. A migrálás a helyesség szempontjából nem kötelező – a nem migrált tevékenységek továbbra is megfelelő eredményeket hoznak – de a migrálás javítja a build teljesítményét.
Az MSBuild korábbi verzióinak támogatása
Ha frissíti az egyéni feladatot, majd terjeszti azt másoknak, a feladat támogatja az MSBuild 18.6-os vagy újabb verzióját használó ügyfeleket. Az MSBuild korábbi verzióiban lévő ügyfelek támogatásához három lehetőség közül választhat.
1. lehetőség: Csökkentett teljesítmény elfogadása
Ne végezzen semmilyen módosítást a feladatán. Az MSBuild nem attribútumokkal rendelkező feladatokat futtat egy másodlagos TaskHost folyamatban, amely lassabb, de teljes mértékben kompatibilis. Ez a beállítás nem igényel kódmódosítást.
2. lehetőség: Különálló implementációk karbantartása
Külön feladatszerelvényeket készíthet az MSBuild 18.6+ és korábbi verzióihoz. Az MSBuild 18.6+ verzió implementálja IMultiThreadableTask és használja TaskEnvironment. A korábbi verzió továbbra is folyamatszintű API-kat használ Task .
3. lehetőség: Kompatibilitási híd
Határozza meg önállóan a MSBuildMultiThreadableTaskAttribute elemet a feladat-összeállításban. Mivel az MSBuild csak névtér és név alapján észleli az attribútumot (a definiáló szerelvény figyelmen kívül hagyásával), az ön által definiált attribútum az MSBuild régi és új verzióiban is működik:
namespace Microsoft.Build.Framework
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}
Ha az MSBuild 18.6-os vagy újabb verzióján fut, az MSBuild felismeri az attribútumot, és folyamatban futtatja a feladatot. Korábbi verziók futtatásakor az MSBuild figyelmen kívül hagyja az ismeretlen attribútumot, és a feladatot a korábbiakhoz hasonlóan futtatja.
Ezzel a lehetőséggel nem fér hozzá a(z) TaskEnvironment elemhez, ezért mindent kézzel kell elvégeznie, amit az kezel, például az összes relatív elérési útvonalat abszolút elérési útvonalakká kell alakítania.
Megközelítések összehasonlítása
Az alábbi táblázat a többszálú módban (-mt) való futtatás három megközelítését hasonlítja össze. Nem többszálú módban az összes tevékenység folyamaton kívül fut, függetlenül attól, hogy azok hogyan vannak megjelölve.
| Approach | Maintenance | Teljesítmény (18,6+) | Teljesítmény (régebbi) | Hozzáférés a TaskEnvironmenthöz |
|---|---|---|---|---|
| Implementációk elkülönítése | Nagy | Teljes folyamatban | Teljes folyamaton kívüli | Igen (18.6+ verzió) |
| Kompatibilitási híd | Low | Teljes feldolgozás alatt | Teljesen folyamaton kívüli | Nem (kizárólag attribútum) |
| Nincsenek módosítások | Nincs | Oldalkocsi (lassabb) | Teljesen folyamaton kívüli | No |