MSBuild görevini çok iş parçacıklı modda çalışacak şekilde güncelleştirin

MSBuild 18.6, aynı işlem içinde paralel olarak derleme özelliğini tanıtır. Bu modu etkinleştirmek için -mt komut satırı anahtarını belirtin. MSBuild'in önceki sürümleri paralel derlemeleri destekliyordu, ancak derlemeler ayrı işlemlerde yapıldı. Bu değişikliğin görevleri yazma biçiminiz üzerinde bazı etkileri vardır. Daha önce görevler ayrı bir işlemde çalıştırılırken, artık çoklu iş parçacığı destekli tüm görevler aynı işlemde çalıştırılır. Çoğu mantığın değişmesi gerekmez ancak daha dikkatli işlenmesi gereken bazı işlem düzeyinde yapılar vardır. İşlem düzeyi yapıları geçerli çalışma dizinini, ortam değişkenlerini ve işlem başlangıç bilgilerini (ProcessStartInfo ) içerir.

Bu değişiklikleri desteklemek için MSBuild 18.6, IMultiThreadableTask arabirimini (Microsoft.Build.Framework) ve TaskEnvironment sınıfını tanıtır. TaskEnvironment, ProjectDirectory özelliğini ve GetAbsolutePath(), GetEnvironmentVariable(), SetEnvironmentVariable() ve GetProcessStartInfo() gibi yöntemleri içerir.

Important

Çok iş parçacıklı mod şu anda deneysel bir özellik olarak mevcuttur; bu aşamada üretim ortamında kullanım için önerilmez. MSBuild kitaplık bağımlılıklarınızı çok iş parçacıklı mod API'lerini kullanacak şekilde güncelleştirmek, kitaplıklarınızın Visual Studio ve MSBuild'in eski sürümlerinde çalışmasını örtük olarak engeller. Erken kullanıcıları çok iş parçacıklı modu denemeye ve geri bildirimde bulunmaya teşvik ediyoruz. sorunları MSBuild GitHub deposunda gönderin.

IMultiThreadableTask arabirimi, çok iş parçacıklı derlemelerde işlem içinde çalışabilen görevler için sözleşmeyi tanımlar:

// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
    TaskEnvironment TaskEnvironment { get; set; }
}

Bir görevi taşımak için, mevcut Task temel sınıfınızın yanında IMultiThreadableTask öğesini uygulayın ve TaskEnvironment özelliğini kullanıma sunun:

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;
    // ...
}

IMultiThreadableTask uygulayan görevler aynı işlem içinde çalışabilir. Bu tür görevlerin [MSBuildMultiThreadableTask] tümü, MSBuild'in görevi işlem içi yürütmeye kabul etmek için kullandığı işaretleyici olan özniteliği de taşımalıdır. özniteliğini eklemeden önce, görevin geçerli çalışma dizini veya ortam gibi işlem düzeyi yapıları üzerinde herhangi bir bağımlılığı olmadığını ve kodunun iş parçacığı açısından güvenli olduğunu onaylayın. Bu değişkenler tüm görev örnekleri arasında paylaşıldığından ve aynı işlemde çalışan görevin farklı örnekleri tarafından erişilebileceği veya değiştirilebileceği için statik değişkenlere iş parçacığı güvenli erişim sağlamaya özellikle dikkat edin.

Örnek görev: BuildCommentTask

Aşağıdaki örnek AddBuildCommentTask , geçiş işlemini göstermek için bu makalenin genelinde kullanılır. Bu görev, metin dosyalarına bir derleme açıklaması ekler. Varsayılan olarak düz metin yazar; isteğe bağlı CommentPrefix ve CommentSuffix özellikleri, çağıranların açıklamayı dile uygun söz diziminde kaydırmasına olanak tanır (örneğin, C#için //, XML için <!-- ve -->, Python veya YAML için #):

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;
        }
    }
}

Proje dosyası, her biri için uygun açıklama söz dizimini geçirerek farklı dosya türleri için bu görevi çağırabilir:

<!-- 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="&lt;!-- "
    CommentSuffix=" --&gt;" />

Bu görev, çok iş parçacıklı derlemeler için çözülmesi gereken dört iş parçacığı güvenliği sorununa sahiptir:

  1. Göreli yollar: File.ReadAllLines ve File.WriteAllLines doğrudan kullanın item.ItemSpec ; göreli bir yol olabilir. Çok iş parçacıklı modda, işlem çalışma dizininin proje dizini olacağı garanti değildir.
  2. Statik alan: ModifiedFileCount Birden çok derleme eşzamanlı olarak çalıştırıldığında veri yarışlarına neden olan tüm örnekler arasında paylaşılan bir static alandır.
  3. Ortam değişkenleri: Çok iş parçacıklı derlemelerde en yaygın ortam değişkeni sorunu, alt süreç başlatmadan önce ortam değişkenlerini ayarlayan ve alt sürecin bunları devralmasını bekleyen görevlerdir. Çok iş parçacıklı modda, Environment.SetEnvironmentVariable() tüm eşzamanlı derlemeler tarafından paylaşılan işlem düzeyindeki ortamı değiştirir; bu nedenle, bir projenin alt işlemi için amaçlanan bir değişiklik başka bir projeninkine de sızabilir. Ortam değişkenlerini doğrudan görev kodunda (Environment.GetEnvironmentVariable()) okumak da genellikle kötü bir uygulamadır; MSBuild özellikleri günlüğe kaydedildiğinden ve izlenebilir olduğundan daha iyi bir alternatiftir.

Important

Çok iş parçacıklı derleme modu şu anda yalnızca CLI (dotnet build ve MSBuild.exe) derlemelerinde kullanılabilir. Visual Studio MSBuild derlemeleri henüz işlem içinde çok iş parçacıklı yürütmeyi desteklememektedir. Visual Studio’da tüm görev yürütme işlemleri ayrı bir işlemde çalışmaya devam eder. Visual Studio tümleştirmesi gelecekteki bir sürüm için planlanıyor.

Prerequisites

  • MSBuild 18.6 veya üzeri.

  • -mt komut satırı anahtarıyla görevlerin çok iş parçacıklı yürütülmesini etkinleştirin:

    dotnet build -mt
    

    -mt anahtarı hakkında daha fazla bilgi için bkz. MSBuild komut satırı başvurusu.

Geçişi planlama

Aşağıdaki sorunlar için görev kodunuzu gözden geçirin:

  1. Görev kodunu denetleyin ve göreli yolların kullanımını belirleyin. Tüm giriş ve dosya G/Ç'lerini denetleyin.
  2. Ortam değişkenlerinin kullanımlarını denetleyin.
  3. ProcessStartInfo API kullanımı olup olmadığını kontrol edin.
  4. Statik alanları veya veri yapılarını denetleyin ve iş parçacığı açısından güvenli hale getirmek için standart yöntemleri kullanın.
  5. Yukarıdakilerden hiçbiri geçerli değilse yalnızca özniteliğini eklemeyi göz önünde bulundurun.
  6. MSBuild'in önceki sürümlerini desteklemek için özel gereksinimleri göz önünde bulundurun. Bkz . MSBuild'in önceki sürümlerini destekleme.

API değişimi için hızlı başvuru kılavuzu

Aşağıdaki tabloda değiştirmeniz gereken .NET API'leri ve bunların TaskEnvironment eşdeğerleri özetlemektedir:

.NET API'den kaçının Level Değiştirme
Path.GetFullPath(path) ERROR Bu tablodan sonraki nota bakın
File.* göreli yollarla ERROR Önce TaskEnvironment.GetAbsolutePath() ile çöz
Directory.* göreli yollarla ERROR Önce TaskEnvironment.GetAbsolutePath() ile çözümle
Environment.GetEnvironmentVariable() ERROR TaskEnvironment.GetEnvironmentVariable()
Environment.SetEnvironmentVariable() ERROR TaskEnvironment.SetEnvironmentVariable()
Environment.CurrentDirectory ERROR TaskEnvironment.ProjectDirectory
new ProcessStartInfo() ERROR TaskEnvironment.GetProcessStartInfo()
Process.Start() ERROR ToolTask veya TaskEnvironment.GetProcessStartInfo() kullanın
Statik alanlar UYARI Örnek alanları veya iş parçacığı güvenli koleksiyonları kullanın

Note

Path.GetFullPath(path) iki şey yapar: göreli bir yolu mutlak yola dönüştürür ve yolun kanonik bir biçimini üretir (. ve .. bölümlerini çözümleyerek). Bunların ayrı olarak işlenmesi gerekir:

  • Yalnızca mutlak yol: kullanın TaskEnvironment.GetAbsolutePath(path). Bu yaklaşım, yolu doğrudan .NET API'lere geçirdiğiniz çoğu dosya G/Ç işlemi için yeterlidir.
  • Kurallı yol: Kurallı biçime güveniyorsanız (örneğin, bir yolu önbellek veya sözlük anahtarı olarak kullanırken), tam olarak çözümlenmiş, kurallı bir mutlak yol elde etmek için kullanın Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path)) .

Görevi öznitelikle işaretleyin

Çok iş parçacıklı derlemelerde yer alan tüm görevler, [MSBuildMultiThreadableTask] özniteliğiyle işaretlenmelidir. Bu öznitelik, MSBuild'in işlem sırasında çalıştırılması güvenli görevleri tanımlamak için kullandığı sinyaldir.

[MSBuildMultiThreadableTask]
public class MyTask : Task
{
    public override bool Execute()
    {
        // Task logic that doesn't depend on process-level state
        return true;
    }
}

Göreviniz zaten iş parçacığı açısından güvenliyse ve işlem düzeyi API'leri (geçerli çalışma dizini, ortam değişkenleri, ProcessStartInfo) kullanmıyorsa tek ihtiyacınız olan özniteliktir. Görev, başka hiçbir değişiklik olmadan Task öğesinden (veya ToolTask öğesinden) devralmaya devam eder.

Görevinizin işlem düzeyi API çağrılarını değiştirmesi gerekiyorsa (örneğin, göreli yolları çözümlemek veya ortam değişkenlerini güvenli bir şekilde okumak için) de uygulayın IMultiThreadableTask. Bu arabirim, görevinize TaskEnvironment özelliğine erişim sağlar. Bu öznitelik her iki durumda da gerekli olmaya devam eder; IMultiThreadableTask, TaskEnvironment API’sinin kilidini açan ek bir adımdır.

Note

MSBuild, tanımlayan derlemeyi yok sayarak MSBuildMultiThreadableTaskAttribute öğesini yalnızca ad alanına ve adına göre algılar. Bu, özniteliği kendi kodunuzda kendiniz tanımlayabileceğiniz anlamına gelir (bkz . MSBuild'in önceki sürümlerini destekleme) ve MSBuild bunu hala tanır.

Note

MSBuildMultiThreadableTaskAttribute devralınamaz (Inherited = false ). Her görev sınıfı, çok iş parçacıklı olarak tanınmak için özniteliği açıkça bildirmelidir. Bu özniteliğe sahip olan bir sınıftan devralmak, türetilmiş sınıfı otomatik olarak çok iş parçacığına uygun hâle getirmez.

TaskEnvironment'ı Geri Dönüşe Başlatma

IMultiThreadableTask uygulanırken, TaskEnvironment özelliğini TaskEnvironment.Fallback olarak başlatın:

public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;

MSBuild, normal bir derlemede çağırmadan Execute() önce bu özelliği ayarlar. Varsayılan Fallback ayar, özelliği ayarlamak için MSBuild'in mevcut olmadığı diğer barındırma senaryolarında (birim testleri veya özel derleme düzenleme araçları gibi) görevin doğru çalışmasını sağlar. Bu olmadan, TaskEnvironment öğesine motorun dışından erişmek null başvuru özel durumuna neden olur.

TaskEnvironment.Fallback içermeyen 18.6'dan önceki MSBuild sürümlerini desteklemeniz gerekiyorsa, özelliği bunun yerine null olarak başlatın ve tüm TaskEnvironment çağrılarını null denetimiyle koruyun. Daha fazla seçenek için bkz. MSBuild'in önceki sürümlerini destekleme .

Yol adlarını ve dosya G/Ç'sini güncelleyin

Bir görev, genellikle MSBuild'deki öğe listeleri gibi, eğer bunlar dosyaysa göreli yol biçiminde olabilecek girdileri kabul eder.

Göreli yollar her zaman işlemin geçerli çalışma dizinine göredir, ancak görev artık işlem içinde yürütülürken, çalışma dizini görev kendi işleminde çalıştırıldığındakiyle aynı olmayabilir. Bu tür yollar proje dizinine göredir. , TaskEnvironment göreli yolları mutlak yollara çözümlemek için kullanabileceğiniz bir ProjectDirectory özellik ve yöntem içerirGetAbsolutePath(). FullPath metadatum’una da erişebilirsiniz; ItemSpec göreli yolu kullanıp ardından bunu mutlak yola dönüştürmenize gerek yoktur.

AbsolutePath türü

AbsolutePath, Microsoft.Build.Framework'da doğrulanmış bir mutlak dosya yolunu temsil eden salt okunur bir yapıdır. Önemli üyeler şunlardır:

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);
}

Oluşturucu, AbsolutePath sağlanan yolun kök dizinine sahip olduğunu doğrular. Göreli bir yol ve taban yol sağlayarak da bir AbsolutePath oluşturabilirsiniz. string'a örtük dönüşüm, string yolu bekleyen herhangi bir API'ye doğrudan bir AbsolutePath geçirebileceğiniz anlamına gelir.

OriginalValue özelliği, orijinal yol dizesini çözümlenmeden önce iletildiği şekliyle korur. Bu özellik, görev çıkışlarında veya günlük iletilerinde göreli yolları tutmanız gerektiğinde kullanışlıdır. Örneğin, hangi dosyaları işlediğini günlüğe kaydeden bir görev, günlük iletilerinde OriginalValue kullanabilir; böylece çıktıdaki yollar göreli ve okunabilir kalırken, gerçek dosya G/Ç işlemleri için çözümlenmiş Value (veya örtük string dönüşümü) kullanılmaya devam edilir.

Öğe yollarını çözümlemek için kullanın TaskEnvironment.GetAbsolutePath() :

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}");

Paralel derlemelerde dosya çakışmasını ele alma

Birden çok görev paralel olarak çalıştırıldığında ve aynı dosyaya eriştiğinde dosya çekişmesi oluşabilir. Bu sorun hem geleneksel çok işlemli model hem de daha yeni işlem içi çok iş parçacıklı mod için geçerlidir. Her iki durumda da aşağıdaki durumlarda aynı dosyaya eşzamanlı olarak erişilebilir:

  • Aynı dosya birden çok alt proje derlemesinde (örneğin, paylaşılan bir yapılandırma dosyası veya bağlı kaynak dosyası) görüntülenir.
  • Görev, başka bir görev örneğinin de işlemekte olduğu bir dosyayı okur ve yazar.

File.ReadAllLines ve File.WriteAllLines gibi yardımcı yöntemler, dosya kilitlemesi üzerinde açık denetim sağlamaz. Eşzamanlı erişim mümkün olduğunda, açık paylaşım ve kilitleme ile kullanın FileStream :

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);
}

Çok iş parçacıklı görevlerde dosya G/Ç için temel yönergeler:

  • Okuma-değiştirme-yazma işlemleri için kullanın FileShare.None . Bu ayar, dosyayı güncelleştirirken başka bir görevin eski içeriği okumasını engeller.
  • IOException öğesini yakalayın ve yeniden denemeyi değerlendirin. Başka bir görev veya işlem kilidi elinde tuttuğunda, açma girişiminiz IOException hatası oluşturur. Geri alma ile kısa bir yeniden deneme genellikle uygundur.
  • Kilitleri aynı anda birden çok dosyada tutmaktan kaçının. İki görevden her biri bir dosyayı kilitler ve sonra diğerini kilitlemeye çalışırsa, bir deadlock oluşur. Birden çok dosya üzerinde çalışmanız gerekiyorsa, bunları tutarlı bir düzende kilitleyin (örneğin, tam yola göre sıralanmış).
  • Kilitleri olabildiğince kısa tutun. Dosyayı açın, tek bir işlemle okuyun, değiştirin, yazın ve kapatın. İlişkili olmayan işler yaparken dosya kilidini tutmayın.

Yukarıdaki örnek bir yaklaşımdır. .NET'te iş parçacığı açısından güvenli dosya G/Ç işlemlerine ilişkin genel yönergeler için FileStream sınıfı, FileShare numaralandırması ve Yönetilen iş parçacığı kullanımı için en iyi uygulamalar bölümüne bakın.

Note

TaskEnvironment kendisi iş parçacığı açısından güvenli değildir. Bu yalnızca, göreviniz kendi içinde kendi iş parçacıklarını oluşturuyorsa (örneğin, Parallel.ForEach veya Task.Run kullanarak) önemlidir. Görevlerin çoğu bunu yapmaz. Execute() öğesini doğrusal olarak uygular ve MSBuild'in görev örnekleri arasındaki paralelliği yönetmesine izin verirler. Göreviniz kendi iş parçacıklarını oluşturuyorsa, onları başlatmadan önce TaskEnvironment içindeki değerleri yerel değişkenlere alın; birden çok iş parçacığından aynı anda TaskEnvironment öğesine erişmeyin.

Ortam değişkenlerini güncelleştirme

Note

Görev kodundaki ortam değişkenlerini okumak, tek iş parçacıklı derlemelerde bile genellikle kötü bir uygulamadır. MSBuild özellikleri daha iyi bir seçenektir: kapsamları açıkça tanımlanmıştır, derleme sırasında günlüğe kaydedilir ve derleme günlüğünde izlenebilir. Göreviniz giriş almak için şu anda bir ortam değişkeni okuyorsa, bunun yerine bir görev özelliğiyle değiştirmeyi göz önünde bulundurun. Proje yine de değeri bir ortam değişkeninden türetebilir: <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.

Bu bölümdeki kılavuz, zaten ortam değişkenlerini kullanan mevcut görevlerin geçirilmesine yöneliktir. Yeniden düzenleme fırsatınız varsa özellikleri ve öğeleri tercih edin.

Alt işlemler için ortam değişkenlerini ayarlama

Çok iş parçacıklı derlemelerde en yaygın ortam değişkeni sorunu, bir ortam değişkenini ayarlayan ve ardından bir alt süreç başlatıp alt sürecin bunu devralmasını bekleyen bir görevdir. Çok işlemli modelde, Environment.SetEnvironmentVariable() bu proje için çalışan işlem ortamını güvenli bir şekilde değiştirdi. Çok iş parçacıklı modda, işlem tüm eşzamanlı derlemelerde paylaşılır, bu nedenle bir projenin alt işlemine yönelik bir değişiklik diğerine sızabilir.

ile birlikte kullanın TaskEnvironment.SetEnvironmentVariable() (bkzTaskEnvironment.GetProcessStartInfo()). GetProcessStartInfo(), SetEnvironmentVariable() ile ayarladığınız değişkenler de dahil olmak üzere, projenin çalışma dizini ve yalıtılmış ortam tablosuyla önceden doldurulmuş bir ProcessStartInfo döndürür; böylece alt süreçler doğru, projeye özgü ortamı otomatik olarak devralır.

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

Mevcut görevlerde ortam değişkenlerini okuma

Mevcut göreviniz ortam değişkenlerini okuyorsa ve bunu hemen görev özelliklerine göre yeniden düzenleyemiyorsanız, Environment.GetEnvironmentVariable() yerine TaskEnvironment.GetEnvironmentVariable() kullanın. Bu yöntem çağrısı paylaşılan işlem ortamı yerine proje kapsamlı ortam tablosundan okur, böylece eşzamanlı derlemeler birbiriyle çakışmaz.

Önce (BuildCommentTask öğesinden):

string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");

After:

string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");

Tavsiye

Ortam değişkenini okuyan mevcut kodu güncelleştirirken, deseni bir görev özelliğiyle değiştirmeyi göz önünde bulundurun. Örneğin, görevde public bool DisableComments { get; set; } kullanıma sunun ve projenin DisableComments="$(DISABLE_BUILD_COMMENTS)" geçirmesine izin verin. MSBuild, çözümlenen değeri günlüğe kaydederek derleme günlüğünde görünür hale getirir ve tanılaması gizli bir ortam değişkeni okumaktan çok daha kolaydır.

ProcessStart API çağrılarını güncelleştirme

Genellikle, bir görev bir işlem başlatıyorsa, her şeyi sizin için halleden ToolTask kullanmalısınız. ProcessStartInfo öğesini doğrudan çağıran bir görevi güncelliyorsanız, TaskEnvironment.GetProcessStartInfo() kullanın. Bu, projenin çalışma dizini ve yalıtılmış ortam tablosuyla yapılandırılmış bir ProcessStartInfo döndürür. Eğer başlatmadan önce ortam değişkenlerini de ayarlıyorsanız, önceki bölümde gösterildiği gibi önce TaskEnvironment.SetEnvironmentVariable() kullanın.

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

Göreviniz ToolTask öğesinden devralıyorsa, işlem başlatma bilgileri sizin için zaten ele alınmıştır. Yalnızca ProcessStartInfo öğesini doğrudan oluşturan görevleri güncellemeniz gerekir.

Statik alanları ve veri yapılarını iş parçacığı açısından güvenli olacak şekilde güncelleştirme

Statik alanlar, çok iş parçacıklı derlemelere geçirilirken dikkatli bir işlem gerektirir. Çok işlemli modelde bile tek bir işlem birden çok proje oluşturabilir, bu nedenle statik durum eşzamanlı olarak paylaşılmaz.

Çok iş parçacıklı mod bu soruna yeni bir boyut ekler. Birden çok derleme artık aynı işlemi paylaşabilir ve görevleri eşzamanlı olarak çalıştırabilir (özellikle çoklu iş parçacığı kullanımıyla otomatik olarak etkinleştirilen MSBuild Sunucusu ile). Statik bir alan, yalnızca derlemenizin içinde değil, aynı anda çalışan ayrı derleme çağrılarında da işlemdeki tüm görev örnekleri arasında paylaşılır. Örneğin, bir derleme sunucusunda aynı anda çalışan dotnet build iki geliştirici veya aynı makinede iki terminal penceresi aynı statik durumu paylaşabilir ve artık bu derlemeler buna aynı anda erişebilir.

BuildCommentTask Örnekte statik alan ModifiedFileCount tüm örnekler arasında paylaşılır:

Before:

private static int ModifiedFileCount = 0;

// In Execute():
ModifiedFileCount++;

Bu kodda iki sorun vardır. İlk olarak, ++ işleç atomik değildir. Birden çok görev örneği aynı anda çalıştığında, iki iş parçacığı aynı değeri okuyabilir ve her ikisi de aynı artımlı sonucu yazabilir ve bu da kayıp sayılarına neden olur. İkincisi, alan statik olduğundan, derlemeler arasında kalıcı olur ve aynı işlemdeki eşzamanlı derlemeler arasında paylaşılır.

Aşağıdaki bölümlerde bu sorunları çözmek için en basitten en doğruya kadar iki yaklaşım gösterilmektedir.

Yaklaşım 1: İş parçacığı açısından güvenli ancak işlem genelinde API kullanma

En basit düzeltme, artışı atomik yapmaktır:

private static int ModifiedFileCount = 0;

// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);

Interlocked.Increment tek bir atomik işlem olarak okuma-artırma-yazma gerçekleştirir, böylece hiçbir sayı kaybolmaz. Bu yaklaşım eşzamanlılık sorununu çözer, ancak sayaç ardışık derlemeler ve eşzamanlı derlemeler de dahil olmak üzere işlemdeki tüm derlemelerde paylaşılır. İki derleme aynı anda çalışırsa dosya numaraları iç içe geçer (Derleme A, #1, #3, #5 numaralarını alır; Derleme B, #2, #4, #6 numaralarını alır). Bu durumun kabul edilebilir olup olmadığı, görevinizin derleme başına yalıtım gerektirip gerektirmediğine bağlıdır. ModifiedFileCount gibi sıralı bir dosya numaralandırma sayacı için, derlemeler arasında paylaşım doğruluk açısından bir sorundur; bunun yerine RegisterTaskObject kullanın (bkz. Yaklaşım 2).

Burada iş parçacığı açısından güvenli, ancak işlem genelini kapsayan eşdeğer API InterlockedIncrement'dır; ancak kendi kodunuzda, iş parçacığı açısından güvenli olmayan tüm API'ler için uygun iş parçacığı güvenli alternatifler bulmanız gerekir. Örneğin, göreviniz durumu kalıcı olarak depolamak için bir Dictionary kullanıyorsa, ConcurrentDictionary<TKey,TValue> kullanmayı göz önünde bulundurun.

Yaklaşım 2: derleme kapsamıyla sınırlı yalıtım için RegisterTaskObject

Göreviniz, tek bir derleme çağrısı içinde alt projeler arasında paylaşılan ancak diğer eşzamanlı derlemelerden yalıtılmış statik durum bilgisi gerektiriyorsa, RegisteredTaskObjectLifetime.Build ile birlikte IBuildEngine4.RegisterTaskObject kullanın. MSBuild, ilk kullanımda oluşturulan ve derleme sona erdiğinde temizlenen nesnenin ömrünü yönetir. Kayıtlı nesnelerin iş parçacığı güvenli olması gerektiğini unutmayın.

İlk olarak, iş parçacığı açısından güvenli basit bir sayaç sınıfı tanımlayın:

internal class FileCounter
{
    private int _count = 0;
    public int Next() => Interlocked.Increment(ref _count);
}

Ardından, sayacı almak veya oluşturmak için çift denetimli kilitleme kullanan yardımcı bir yöntem kullanın:

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;
}

Execute()'da:

FileCounter counter = GetOrCreateCounter();
// ...
int fileNumber = counter.Next();

Bu yaklaşımla, her derleme çağrısının kendine ait bir FileCounter olur. Aynı derlemedeki tüm alt projeler sayacı paylaşır (sıralı numaralandırma), ancak aynı makinede aynı anda çalışan ayrı dotnet build bir sayaç farklı bir sayaç alır. RegisteredTaskObjectLifetime.Build, MSBuild’e nesneyi geçerli derleme çağrısı kapsamıyla sınırlandırmasını ve derleme sona erdiğinde temizlemesini söyler.

Doğru yaklaşımı seçin

Statik durumun nasıl işleneceğini belirlerken şu sorudan başlayın: Bu veriler ardışık derlemeler ve eşzamanlı derlemeler de dahil olmak üzere aynı işlemde çalışabilecek tüm derlemelerde paylaşılması güvenli mi?

MSBuild çalışan süreçleri çağrılar arasında varlığını sürdürür (düğüm yeniden kullanımı varsayılan olarak açıktır) ve bir MSBuild süreci, yalnızca tek bir dotnet build çağrısı sırasında değil, yaşam süresi boyunca birden çok çözüm derlemesini gerçekleştirebilir. İşlemin yalnızca bir derlemeyi işlediğini varsaymayın.

Şu yönergeleri kullanın:

  • Statik alanı yalnızca önbelleğe alınan verilerin farklı projelerde ve birden çok derlemede birden çok iş parçacığından, derlemeler arasında geçersiz kılmaya gerek kalmadan erişilmesi güvenliyse koruyun. Örneğin, hiçbir zaman değişmemiş girişlerden (başlangıçta bir kez yüklenen derleme meta verileri gibi) bir kez hesaplanan sabit verilerin önbelleği uygun olabilir.
  • Her derleme çağrısı için durumun yalıtılması gerektiğinde (örneğin, derlemeler arasında sıfırlanması veya eşzamanlı derlemeler arasında sızmaması gereken sayaçlar, biriktiriciler ya da önbellekler için) IBuildEngine4.RegisterTaskObject öğesini RegisteredTaskObjectLifetime.Build ile kullanın. Bu, çoğu paylaşılan değiştirilebilir durum için tercih edilen yaklaşımdır.
  • Korunan statik durumu iş parçacığı açısından güvenli hâle getirmek için System.Threading ilkel öğeleri (Interlocked, ConcurrentDictionary, lock, ReaderWriterLockSlim) kullanın, ancak iş parçacığı güvenliğinin tek başına derleme düzeyinde yalıtım sağlamadığını unutmayın. Bkz. Yönetilen iş parçacığı oluşturma en iyi yöntemleri.

Tavsiye

Bu makalenin ilerleyen bölümlerindeki tam geçiş örneği, derleme kapsamındaki yalıtımı göstermek için RegisterTaskObject yaklaşımını kullanır.

Tam geçiş örneği

Aşağıdaki kod, beş değişikliğin de uygulandığı tam AddBuildCommentTask geçişi gösterir:

  1. [MSBuildMultiThreadableTask] özniteliğine sahiptir ve onu işlem içi yürütme için işaretler.
  2. Mevcut Task temel sınıfına ek olarak IMultiThreadableTask’ı uygular ve TaskEnvironment özelliğini kullanıma sunar.
  3. Yol çözümlemesi için TaskEnvironment.GetAbsolutePath() kullanır.
  4. yerine TaskEnvironment.GetEnvironmentVariable()kullanırEnvironment.GetEnvironmentVariable().
  5. IBuildEngine4.RegisterTaskObject, dosya sayacını geçerli derleme çağrısıyla sınırlamak için RegisteredTaskObjectLifetime.Build ile kullanılır ve işlem genelindeki statik sayacın yerini alır.
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;
        }
    }
}

Aktarılmayan görevlere ne olur?

Özniteliği olmayan veya uygulamayan [MSBuildMultiThreadableTask]IMultiThreadableTask görevler herhangi bir değişiklik yapmadan çalışmaya devam ediyor. MSBuild bu görevleri, MSBuild'in önceki sürümleriyle aynı işlem düzeyinde yalıtım sağlayan bir yan TaskHost işlemde çalıştırır. İşlemler arası iletişim yükü nedeniyle bu yaklaşım daha yavaştır, ancak mevcut görev koduyla tamamen uyumludur. Geçiş, doğruluk için isteğe bağlıdır; geçirilmeyen görevler yine de doğru sonuçlar üretir, ancak geçiş yapı performansını artırır.

MSBuild'in önceki sürümlerini destekleme

Özel görevinizi güncelleştirir ve sonra başkalarına dağıtırsanız, göreviniz MSBuild 18.6 veya üzerini kullanan istemcileri destekler. MSBuild'in önceki sürümlerinde istemcileri desteklemek için üç seçeneğiniz vardır.

1. Seçenek: Düşük performansı kabul etme

Görevinizde değişiklik yapma. MSBuild, daha yavaş ama tamamen uyumlu olan bir yan TaskHost işlemde özniteliksiz görevler çalıştırır. Bu seçenek kod değişikliği gerektirmez.

2. Seçenek: Ayrı uygulamaları koruma

MSBuild 18.6+ ve önceki sürümler için ayrı görev derlemeleri oluşturun. MSBuild 18.6+ sürümü uygular IMultiThreadableTask ve kullanır TaskEnvironment. Önceki sürüm, işlem düzeyindeki API'lerle birlikte Task kullanmaya devam eder.

Seçenek 3: Uyumluluk köprüsü

MSBuildMultiThreadableTaskAttribute öğesini görev derlemenizde kendiniz tanımlayın. MSBuild özniteliği yalnızca ad alanına ve ada göre algıladığı için (tanımlama derlemesini yoksayarak), kendi kendine tanımlanan özniteliğiniz hem eski hem de yeni MSBuild sürümlerinde çalışır:

namespace Microsoft.Build.Framework
{
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}

MSBuild 18.6 veya sonraki bir sürümde çalışırken, MSBuild özniteliğini tanır ve görevi işlem içinde çalıştırır. MSBuild, önceki sürümlerde çalışırken bilinmeyen özniteliği yoksayar ve görevi daha önce olduğu gibi çalıştırır.

Bu seçenekle, öğesine TaskEnvironmenterişiminiz yoktur, bu nedenle tüm göreli yollarınızı mutlak yollara dönüştürme gibi işlediği her şeyi el ile işlemeniz gerekir.

Yaklaşımların karşılaştırması

Aşağıdaki tabloda, çok iş parçacıklı modda (-mt ) çalıştırılırken kullanılan üç yaklaşım karşılaştırılır. Çoklu iş parçacıklı olmayan modda, nasıl işaretlendiklerinden bağımsız olarak tüm görevler işlem dışı çalışır.

Yaklaşım Bakım Performans (18.6+) Performans (önceki sürüm) TaskEnvironment erişimi
Ayrı uygulamalar Yüksek Tamamen işlem içi Tamamen işlem dışı Evet (18.6+ sürüm)
Uyumluluk köprüsü Low Tam işlem içi Tamamen işlem dışı Hayır (yalnızca öznitelik)
Değişiklik yok Hiçbiri Sidecar (daha yavaş) Tamamen işlem dışı No