Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
MSBuild 18.6 memperkenalkan kemampuan untuk membangun secara paralel dalam proses yang sama. Untuk mengaktifkan mode ini, sertakan opsi baris perintah -mt. Versi MSBuild sebelumnya mendukung build paralel, tetapi build dilakukan di proses yang terpisah. Perubahan ini berdampak pada cara Anda menulis tugas. Sedangkan sebelumnya, tugas akan berjalan dalam proses terpisah, sekarang semua tugas berkemampuan multithread berjalan dalam proses yang sama. Meskipun sebagian besar logika tidak perlu berubah, ada beberapa konstruksi tingkat proses yang perlu ditangani dengan lebih hati-hati. Konstruksi tingkat proses mencakup direktori kerja saat ini, variabel lingkungan, dan info mulai proses (ProcessStartInfo).
Untuk mendukung perubahan ini, MSBuild 18.6 memperkenalkan antarmuka IMultiThreadableTask (dalam Microsoft.Build.Framework) dan kelas TaskEnvironment.
TaskEnvironment
ProjectDirectory termasuk properti dan metode seperti GetAbsolutePath(), , GetEnvironmentVariable()SetEnvironmentVariable(), dan GetProcessStartInfo().
Important
Mode multithreaded saat ini tersedia sebagai fitur eksperimental; saat ini tidak disarankan untuk penggunaan produksi. Memperbarui dependensi pustaka MSBuild Anda untuk menggunakan API mode multithreaded secara implisit mencegah pustaka Anda berjalan pada versi Visual Studio dan MSBuild yang lebih lama. Kami mendorong pengadopsi awal untuk mencoba mode multithreaded, dan memberikan umpan balik. Kirimkan masalah di repositori MSBuild GitHub.
Antarmuka IMultiThreadableTask menentukan kontrak untuk tugas yang dapat berjalan dalam proses dalam build multithreaded:
// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
TaskEnvironment TaskEnvironment { get; set; }
}
Untuk memigrasikan tugas, terapkan IMultiThreadableTask bersama kelas dasar Task yang sudah ada dan ekspos properti TaskEnvironment:
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;
// ...
}
Tugas yang mengimplementasikan IMultiThreadableTask dapat berjalan dalam proses. Semua tugas tersebut juga harus memiliki atribut [MSBuildMultiThreadableTask], yang merupakan penanda yang digunakan MSBuild untuk menetapkan tugas agar dijalankan dalam proses. Sebelum menambahkan atribut, pastikan bahwa tugas tersebut tidak memiliki ketergantungan pada konstruk tingkat proses seperti direktori kerja saat ini atau variabel lingkungan, serta bahwa kodenya bersifat thread-safe. Berikan perhatian khusus untuk memastikan akses yang aman dalam lingkungan multithread ke variabel statis, karena variabel ini digunakan bersama oleh semua instans tugas dan mungkin diakses atau diubah oleh instans tugas lain yang juga berjalan dalam proses yang sama.
Contoh tugas: BuildCommentTask
Contoh AddBuildCommentTask berikut digunakan di seluruh artikel ini untuk mengilustrasikan proses migrasi. Tugas ini menambahkan komentar build ke file teks. Secara default, ia menulis teks biasa; properti CommentPrefix dan CommentSuffix opsional memungkinkan penelepon membungkus komentar dalam sintaksis yang sesuai bahasa (misalnya, // untuk C#, <!-- dan --> untuk XML, # untuk Python atau 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;
}
}
}
File proyek mungkin memanggil tugas ini untuk jenis file yang berbeda, meneruskan sintaks komentar yang sesuai untuk masing-masing:
<!-- 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=" -->" />
Tugas ini memiliki empat masalah terkait keamanan thread yang perlu ditangani untuk build multithread:
-
Jalur relatif:
File.ReadAllLinesdanFile.WriteAllLinesgunakanitem.ItemSpecsecara langsung, yang mungkin merupakan jalur relatif. Dalam mode multithreaded, direktori kerja proses tidak dijamin sebagai direktori proyek. -
Bidang statis:
ModifiedFileCountadalah bidang yangstaticdibagikan di semua instans, yang menyebabkan perlombaan data saat beberapa build berjalan bersamaan. -
Variabel lingkungan: Masalah variabel lingkungan yang paling umum dalam build multithread adalah proses yang menetapkan variabel lingkungan sebelum membuat proses turunan, dengan harapan proses turunan tersebut akan mewarisinya. Dalam mode multithread,
Environment.SetEnvironmentVariable()memodifikasi lingkungan tingkat proses yang digunakan bersama oleh semua proses build yang berjalan secara bersamaan, sehingga perubahan yang dimaksudkan untuk proses turunan milik satu proyek dapat merembet ke proses build proyek lain. Membaca variabel lingkungan secara langsung dalam kode tugas (Environment.GetEnvironmentVariable()) juga umumnya merupakan praktik yang buruk; Properti MSBuild adalah alternatif yang lebih baik karena dicatat dan dapat dilacak.
Important
Mode build multithreaded saat ini hanya tersedia untuk build CLI (dotnet build dan MSBuild.exe). Visual Studio build MSBuild belum mendukung eksekusi multithreaded dalam proses. Di Visual Studio, semua eksekusi tugas terus berjalan di luar proses. integrasi Visual Studio direncanakan untuk rilis mendatang.
Prasyarat
MSBuild 18.6 atau yang lebih baru.
Aktifkan eksekusi tugas multithread dengan sakelar baris perintah
-mt:dotnet build -mtUntuk informasi selengkapnya tentang sakelar
-mt, lihat Referensi baris perintah MSBuild.
Merencanakan migrasi
Tinjau kode tugas Anda untuk masalah berikut:
- Periksa kode tugas dan identifikasi setiap penggunaan path relatif. Periksa semua input dan file I/O.
- Periksa apakah ada penggunaan variabel lingkungan.
- Periksa apakah ada penggunaan API
ProcessStartInfo. - Periksa field statis atau struktur data apa pun dan gunakan metode standar untuk membuatnya aman untuk thread.
- Jika tidak ada hal di atas yang berlaku, pertimbangkan untuk menambahkan atribut saja.
- Pertimbangkan persyaratan khusus untuk mendukung versi MSBuild yang lebih lama. Lihat Mendukung versi MSBuild yang lebih lama.
Referensi cepat pengganti API
Tabel berikut merangkum API .NET yang harus Anda ganti dan padanan TaskEnvironment-nya:
| API .NET yang harus dihindari | Tingkat | Replacement |
|---|---|---|
Path.GetFullPath(path) |
KESALAHAN | Lihat catatan setelah tabel ini |
File.* dengan jalur relatif |
KESALAHAN | Selesaikan dengan TaskEnvironment.GetAbsolutePath() terlebih dahulu |
Directory.* dengan jalur relatif |
KESALAHAN | Selesaikan dengan TaskEnvironment.GetAbsolutePath() terlebih dahulu |
Environment.GetEnvironmentVariable() |
KESALAHAN | TaskEnvironment.GetEnvironmentVariable() |
Environment.SetEnvironmentVariable() |
KESALAHAN | TaskEnvironment.SetEnvironmentVariable() |
Environment.CurrentDirectory |
KESALAHAN | TaskEnvironment.ProjectDirectory |
new ProcessStartInfo() |
KESALAHAN | TaskEnvironment.GetProcessStartInfo() |
Process.Start() |
KESALAHAN | Gunakan ToolTask atau TaskEnvironment.GetProcessStartInfo() |
| Bidang statis | PERINGATAN | Gunakan field instans atau koleksi yang aman untuk thread |
Note
Path.GetFullPath(path) melakukan dua hal: ini mengonversi jalur relatif ke jalur absolut, dan menghasilkan bentuk kanonis jalur (menyelesaikan . dan .. segmen). Ini perlu ditangani secara terpisah:
-
Jalur absolut saja: Gunakan
TaskEnvironment.GetAbsolutePath(path). Pendekatan ini cukup untuk sebagian besar operasi I/O file tempat Anda melewati jalur langsung ke API .NET. -
Jalur kanonis: Jika Anda mengandalkan formulir kanonis (misalnya, saat menggunakan jalur sebagai cache atau kunci kamus), gunakan
Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path))untuk mendapatkan jalur absolut kanonis yang diselesaikan sepenuhnya.
Menandai tugas dengan atribut
Semua tugas yang berpartisipasi dalam build multithreaded harus ditandai dengan [MSBuildMultiThreadableTask] atribut . Atribut ini adalah sinyal yang digunakan MSBuild untuk mengidentifikasi tugas yang aman untuk dijalankan dalam proses.
[MSBuildMultiThreadableTask]
public class MyTask : Task
{
public override bool Execute()
{
// Task logic that doesn't depend on process-level state
return true;
}
}
Jika tugas Anda sudah aman terhadap thread dan Anda tidak menggunakan API tingkat proses apa pun (direktori kerja saat ini, variabel lingkungan, ProcessStartInfo), atribut tersebut saja sudah cukup. Tugas tetap mewarisi dari Task (atau ToolTask) tanpa perubahan lain.
Jika tugas Anda memang perlu mengganti panggilan API tingkat proses (misalnya, untuk mengatasi jalur relatif atau membaca variabel lingkungan dengan aman), terapkan IMultiThreadableTaskjuga . Antarmuka ini memberi tugas Anda akses ke properti TaskEnvironment. Atribut tetap diperlukan dalam kedua kasus; IMultiThreadableTask adalah langkah tambahan yang membuka kunci TaskEnvironment API.
Note
MSBuild mendeteksi MSBuildMultiThreadableTaskAttribute hanya berdasarkan namespace dan nama, tanpa memperhatikan assembly yang mendefinisikannya. Ini berarti Anda dapat menentukan atribut sendiri dalam kode Anda sendiri (lihat Mendukung versi MSBuild sebelumnya) dan MSBuild masih mengenalinya.
Note
MSBuildMultiThreadableTaskAttribute tidak dapat diwariskan (Inherited = false). Setiap kelas tugas harus secara eksplisit menyatakan atribut yang akan dikenali sebagai multithreadable. Mewarisi dari kelas yang memiliki atribut tersebut tidak secara otomatis membuat kelas turunan mendukung multithreading.
Menginisialisasi TaskEnvironment ke Fallback
Saat menerapkan IMultiThreadableTask, inisialisasi TaskEnvironment properti ke TaskEnvironment.Fallback:
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
MSBuild mengatur properti ini sebelum memanggil Execute() dalam build normal.
Fallback default memastikan bahwa tugas berfungsi dengan benar dalam skenario host lainnya (seperti pengujian unit atau alat orkestrasi build kustom) ketika MSBuild tidak tersedia untuk menetapkan properti tersebut. Tanpa itu, mengakses TaskEnvironment di luar mesin akan menyebabkan pengecualian referensi nol.
Jika Anda perlu mendukung versi MSBuild yang lebih lama dari 18.6 dan tidak menyertakan TaskEnvironment.Fallback, inisialisasi properti menjadi null sebagai gantinya dan lindungi setiap pemanggilan TaskEnvironment dengan pemeriksaan null. Lihat Mendukung versi MSBuild yang lebih lama untuk opsi lainnya.
Memperbarui jalur dan I/O file
Tugas sering menerima input, seperti daftar item di MSBuild, yang jika mereka adalah file, mungkin dalam bentuk jalur relatif.
Jalur relatif selalu relatif terhadap direktori kerja proses saat ini, tetapi karena tugas sekarang dijalankan dalam proses, direktori kerja mungkin tidak sama seperti ketika tugas berjalan dalam prosesnya sendiri. Jalur tersebut relatif terhadap direktori proyek.
TaskEnvironment menyertakan ProjectDirectory properti dan GetAbsolutePath() metode yang dapat Anda gunakan untuk menyelesaikan jalur relatif ke jalur absolut. Anda juga dapat mengakses FullPath metadata; tidak perlu menggunakan jalur relatif ItemSpec lalu mengubahnya menjadi jalur absolut.
Jenis AbsolutePath
AbsolutePath adalah struktur baca-saja di Microsoft.Build.Framework yang mewakili jalur file absolut yang divalidasi. Anggota utama meliputi:
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);
}
AbsolutePath Konstruktor memvalidasi bahwa jalur yang disediakan berakar. Anda juga dapat membangun AbsolutePath dengan menyediakan jalur relatif dan jalur dasar. Konversi implisit ke string berarti Anda dapat meneruskan AbsolutePath secara langsung ke API mana pun yang mengharapkan path string.
Properti OriginalValue mempertahankan string jalur asli sebagaimana diteruskan sebelum diresolusikan. Properti ini berguna ketika Anda perlu menyimpan jalur relatif dalam output tugas atau pesan log. Misalnya, tugas yang mencatat file mana saja yang diproses dapat menggunakan OriginalValue dalam pesan lognya agar path dalam output tetap relatif dan mudah dibaca, sementara tetap menggunakan Value yang sudah di-resolve (atau konversi implisit string) untuk operasi I/O file yang sebenarnya.
Gunakan TaskEnvironment.GetAbsolutePath() untuk menyelesaikan path item:
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}");
Menangani perselisihan file dalam build paralel
Pertikaian file dapat terjadi setiap kali beberapa tugas berjalan secara paralel dan mengakses file yang sama. Kekhawatiran ini berlaku baik untuk model multi-proses tradisional maupun mode multithread dalam proses yang lebih baru. Dalam kedua kasus, file yang sama mungkin diakses secara bersamaan ketika:
- File yang sama muncul di beberapa build sub-proyek (misalnya, file konfigurasi bersama atau file sumber tertaut).
- Tugas membaca dan menulis file yang juga diproses oleh instans tugas lain.
Metode kenyamanan seperti File.ReadAllLines dan File.WriteAllLines tidak memberikan kontrol eksplisit atas penguncian file. Jika akses bersamaan dimungkinkan, gunakan FileStream dengan berbagi dan penguncian eksplisit:
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);
}
Panduan utama untuk I/O file dalam tugas multithread:
- Gunakan
FileShare.Noneuntuk operasi baca-ubah-tulis. Pengaturan ini mencegah tugas lain membaca isi kedaluarsa saat Anda memperbarui file. - Tangani
IOExceptiondan pertimbangkan untuk coba lagi. Ketika tugas atau proses lain sedang mengunci, upaya Anda untuk membuka akan menghasilkanIOException. Percobaan ulang singkat dengan backoff sering kali sesuai. - Hindari memegang kunci pada beberapa file sekaligus. Jika dua tugas masing-masing mengunci satu file dan kemudian mencoba mengunci yang lain, Anda mendapatkan kebuntuan. Jika Anda harus menangani beberapa file, kunci file-file tersebut dalam urutan yang konsisten (misalnya, diurutkan berdasarkan jalur lengkapnya).
- Jaga kunci sesingkat mungkin. Buka file, baca, ubah, tulis, dan tutup dalam satu operasi. Jangan menahan kunci file saat melakukan pekerjaan yang tidak terkait.
Contoh sebelumnya adalah salah satu pendekatan. Untuk panduan umum tentang I/O file yang aman untuk thread di .NET, lihat kelas FileStream, enum FileShare, dan praktik terbaik threading terkelola.
Note
TaskEnvironment itu sendiri tidak aman utas. Ini hanya penting jika tugas Anda secara internal membuat utas sendiri (misalnya, menggunakan Parallel.ForEach atau Task.Run). Sebagian besar tugas tidak melakukan ini. Mereka menerapkan Execute() secara linier dan membiarkan MSBuild menangani paralelisme di seluruh instans tugas. Jika tugas Anda memang membuat utasnya sendiri, tangkap nilai dari TaskEnvironment ke dalam variabel lokal sebelum menjalankannya, alih-alih mengakses TaskEnvironment dari beberapa utas secara bersamaan.
Memperbarui variabel lingkungan
Note
Membaca variabel lingkungan dalam kode tugas umumnya merupakan praktik yang buruk, bahkan dalam build utas tunggal. Properti MSBuild adalah alternatif yang lebih baik: properti tersebut secara eksplisit dicakup, dicatat selama build, dan dapat dilacak di log build. Jika tugas Anda saat ini membaca variabel lingkungan untuk menerima input, pertimbangkan untuk menggantinya dengan properti tugas sebagai gantinya. Proyek ini masih dapat memperoleh nilai dari variabel lingkungan: <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.
Panduan di bagian ini adalah untuk memigrasikan tugas yang ada yang sudah mengandalkan variabel lingkungan. Jika Anda berkesempatan melakukan refaktor, utamakan properti dan item.
Mengatur variabel lingkungan untuk proses anak
Masalah variabel lingkungan yang paling umum dalam build multithread adalah proses yang mengatur variabel lingkungan lalu memunculkan proses turunan, dengan harapan proses turunan tersebut mewarisinya. Dalam model multiproses, Environment.SetEnvironmentVariable() modifikasi lingkungan proses pekerja dengan aman untuk proyek tersebut. Dalam mode multithreaded, proses dibagikan di semua build bersamaan, sehingga perubahan yang ditujukan untuk proses anak satu proyek dapat bocor ke yang lain.
Gunakan TaskEnvironment.SetEnvironmentVariable() bersama dengan TaskEnvironment.GetProcessStartInfo() (lihat Memperbarui panggilan API ProcessStart).
GetProcessStartInfo() mengembalikan ProcessStartInfo yang telah diisi sebelumnya dengan direktori kerja proyek dan tabel lingkungan terisolasinya, termasuk variabel apa pun yang Anda tetapkan dengan SetEnvironmentVariable(), sehingga proses anak secara otomatis mewarisi lingkungan yang benar dan khusus untuk proyek.
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
Membaca variabel lingkungan dalam tugas yang ada
Jika tugas yang ada membaca variabel lingkungan dan Anda tidak dapat segera merefaktor ke properti tugas, ganti Environment.GetEnvironmentVariable() dengan TaskEnvironment.GetEnvironmentVariable(). Panggilan metode ini membaca dari tabel lingkungan yang dicakup proyek daripada lingkungan proses bersama, sehingga build bersamaan tidak mengganggu satu sama lain.
Sebelum (dari BuildCommentTask):
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
After:
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Tip
Saat memperbarui kode yang ada yang membaca variabel lingkungan, pertimbangkan untuk mengganti pola dengan properti tugas. Misalnya, tampilkan public bool DisableComments { get; set; } pada tugas dan biarkan proyek meneruskan DisableComments="$(DISABLE_BUILD_COMMENTS)". MSBuild mencatat nilai hasil resolusi, sehingga nilainya terlihat dalam log build dan jauh lebih mudah ditelusuri penyebab masalahnya dibandingkan dengan pembacaan variabel lingkungan yang tersembunyi.
Memperbarui panggilan API ProcessStart
Jika suatu tugas memulai proses, biasanya Anda harus menggunakan ToolTask, yang menangani semuanya. Dalam kasus di mana Anda memperbarui tugas yang memanggil ProcessStartInfo secara langsung, gunakan TaskEnvironment.GetProcessStartInfo(). Ini mengembalikan ProcessStartInfo yang dikonfigurasi dengan direktori kerja proyek dan tabel lingkungan yang terisolasi. Jika Anda juga mengatur variabel lingkungan sebelum meluncurkan, gunakan TaskEnvironment.SetEnvironmentVariable() terlebih dahulu, seperti yang ditunjukkan di bagian sebelumnya.
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
Jika tugas Anda mewarisi dari ToolTask, informasi awal proses sudah ditangani untuk Anda. Anda hanya perlu memperbarui tugas yang secara langsung membuat ProcessStartInfo.
Perbarui atribut statis dan struktur data agar aman terhadap thread
Bidang statis memerlukan perawatan yang cermat ketika Anda bermigrasi ke build multithreaded. Bahkan dalam model multi-proses, satu proses dapat membangun beberapa proyek, sehingga status statis dibagikan, hanya tidak secara bersamaan.
Mode multithreaded menambahkan dimensi baru ke masalah ini. Beberapa build sekarang dapat berbagi proses yang sama dan menjalankan tugas secara bersamaan (terutama dengan MSBuild Server, yang secara otomatis diaktifkan dengan multithreading). Field statis digunakan bersama oleh semua instans tugas dalam proses tersebut, bukan hanya di dalam build Anda, tetapi juga berpotensi di antara pemanggilan build terpisah yang berjalan secara bersamaan. Misalnya, dua pengembang yang berjalan dotnet build pada saat yang sama pada server build, atau dua jendela terminal pada komputer yang sama, mungkin berbagi status statis yang sama, dan sekarang build tersebut mengaksesnya secara bersamaan.
Dalam contoh BuildCommentTask, field statis ModifiedFileCount digunakan bersama oleh semua instance:
Before:
private static int ModifiedFileCount = 0;
// In Execute():
ModifiedFileCount++;
Kode ini memiliki dua masalah. Pertama, ++ operator bukan atomik. Saat beberapa instans tugas berjalan secara bersamaan, dua utas dapat membaca nilai yang sama dan keduanya menulis nilai hasil penambahan yang sama, sehingga menyebabkan hitungan hilang. Kedua, karena field tersebut bersifat statis, field tersebut tetap ada di seluruh build dan digunakan bersama oleh build yang berjalan secara bersamaan dalam proses yang sama.
Bagian berikut menunjukkan dua pendekatan untuk memperbaiki masalah ini, dari yang paling sederhana hingga yang paling benar.
Pendekatan 1: Gunakan API yang aman terhadap utas, tetapi berlaku untuk seluruh proses
Solusi paling sederhana adalah membuat operasi increment bersifat atomik:
private static int ModifiedFileCount = 0;
// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);
Interlocked.Increment melakukan read-increment-write sebagai satu operasi atomik, sehingga tidak ada nilai hitungan yang hilang. Pendekatan ini mengatasi masalah konkurensi, tetapi penghitung tersebut masih digunakan bersama oleh semua build dalam suatu proses, termasuk build berurutan dan build yang berjalan secara bersamaan. Jika dua build berjalan secara bersamaan, nomor filenya akan berselang-seling (Build A mendapatkan #1, #3, #5; Build B mendapatkan #2, #4, #6). Apakah situasi ini dapat diterima bergantung pada apakah tugas Anda memerlukan isolasi untuk setiap build. Untuk penghitung penomoran file berurutan seperti ModifiedFileCount, berbagi antar-build merupakan masalah keakuratan; gunakan RegisterTaskObject sebagai gantinya (lihat Pendekatan 2).
Di sini, padanan API yang thread-safe tetapi berlaku untuk seluruh proses adalah InterlockedIncrement, tetapi dalam kode Anda sendiri, Anda perlu mencari alternatif thread-safe yang sesuai untuk API apa pun yang tidak thread-safe. Misalnya, jika tugas Anda mempertahankan status menggunakan Dictionary, pertimbangkan untuk menggunakan ConcurrentDictionary<TKey,TValue>.
Pendekatan 2: RegisterTaskObject untuk isolasi lingkup build
Jika tugas Anda memerlukan status statis yang dibagikan di seluruh sub-proyek dalam satu pemanggilan build tetapi diisolasi dari build bersamaan lainnya, gunakan IBuildEngine4.RegisterTaskObject dengan RegisteredTaskObjectLifetime.Build. MSBuild mengelola masa pakai objek, yang dibuat pada penggunaan pertama dan dibersihkan saat build berakhir. Perhatikan bahwa objek yang didaftarkan harus aman untuk thread.
Pertama, definisikan kelas penghitung thread-safe sederhana:
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
Kemudian gunakan metode pembantu dengan penguncian yang diperiksa ganda untuk mendapatkan atau membuat penghitung:
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;
}
Dalam Execute():
FileCounter counter = GetOrCreateCounter();
// ...
int fileNumber = counter.Next();
Dengan pendekatan ini, setiap pemanggilan build memiliki FileCounter sendiri. Semua sub-proyek dalam proses build yang sama menggunakan penghitung yang sama (penomoran berurutan), tetapi dotnet build terpisah yang berjalan pada saat yang sama di mesin yang sama menggunakan penghitung yang berbeda.
RegisteredTaskObjectLifetime.Build memberi tahu MSBuild untuk membatasi cakupan objek pada proses build saat ini dan membersihkannya ketika proses build berakhir.
Pilih pendekatan yang tepat
Saat memutuskan cara menangani status statis, mulai dari pertanyaan ini: apakah data ini aman untuk dibagikan di semua build yang mungkin pernah berjalan dalam proses yang sama, termasuk build berturut-turut dan build bersamaan?
Proses pekerja MSBuild tetap berjalan lintas pemanggilan (penggunaan ulang node aktif secara default), dan proses MSBuild berpotensi menangani beberapa build solusi selama masa pakainya, bukan hanya dalam satu pemanggilan dotnet build. Jangan berasumsi bahwa proses hanya menangani satu build.
Gunakan panduan ini:
- Pertahankan bidang statis hanya jika data yang di-cache aman untuk diakses dari beberapa utas di berbagai proyek dan di beberapa build tanpa memerlukan pembatalan antar build. Misalnya, cache data yang tidak dapat diubah dihitung sekali dari input yang tidak pernah berubah (seperti metadata rakitan yang dimuat sekali saat startup) mungkin memenuhi syarat.
-
Gunakan
IBuildEngine4.RegisterTaskObjectdenganRegisteredTaskObjectLifetime.Buildjika state harus diisolasi untuk setiap pemanggilan build (misalnya, penghitung, akumulator, atau cache yang harus diatur ulang di antara build atau tidak bocor di antara build yang berjalan secara bersamaan). Ini adalah pendekatan yang lebih disarankan untuk sebagian besar keadaan bersama yang dapat diubah. -
Gunakan
System.Threadingprimitif (Interlocked,ConcurrentDictionary,lock,ReaderWriterLockSlim) untuk membuat status statis yang dipertahankan menjadi aman untuk thread, tetapi ingat bahwa keamanan thread saja tidak menyediakan isolasi pada tingkat build. Lihat Praktik terbaik utas terkelola.
Tip
Contoh migrasi lengkap selanjutnya dalam artikel ini menggunakan pendekatan RegisterTaskObject untuk menunjukkan isolasi pada tingkat build.
Contoh migrasi lengkap
Kode berikut menunjukkan migrasi AddBuildCommentTask penuh dengan kelima perubahan yang diterapkan:
- Memiliki atribut
[MSBuildMultiThreadableTask], yang menandainya untuk eksekusi dalam-proses. - Mengimplementasikan
IMultiThreadableTaskbersama kelas dasarTaskyang sudah ada, dan mengekspos propertiTaskEnvironment. - Menggunakan
TaskEnvironment.GetAbsolutePath()untuk resolusi jalur. - Menggunakan
TaskEnvironment.GetEnvironmentVariable()alih-alihEnvironment.GetEnvironmentVariable(). - Menggunakan
IBuildEngine4.RegisterTaskObjectbersamaRegisteredTaskObjectLifetime.Builduntuk membatasi cakupan penghitung file pada pemanggilan build saat ini, menggantikan penghitung statis yang berlaku untuk seluruh proses.
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;
}
}
}
Apa yang terjadi pada tugas yang tidak dimigrasikan
Tugas yang tidak memiliki [MSBuildMultiThreadableTask] atribut atau tidak menerapkan IMultiThreadableTask terus berfungsi tanpa perubahan apa pun. MSBuild menjalankan tugas-tugas ini dalam proses anak perusahaan TaskHost , yang menyediakan isolasi tingkat proses yang sama dengan versi MSBuild sebelumnya. Pendekatan ini lebih lambat karena overhead komunikasi antarproses, tetapi sepenuhnya kompatibel dengan kode tugas yang ada. Migrasi bersifat opsional untuk kebenaran—tugas yang tidak dimigrasikan masih menghasilkan hasil yang benar—tetapi migrasi meningkatkan performa build.
Mendukung versi MSBuild yang lebih lama
Jika Anda memperbarui tugas kustom lalu mendistribusikannya ke orang lain, tugas Anda mendukung klien menggunakan MSBuild 18.6 atau yang lebih baru. Untuk mendukung klien pada versi MSBuild yang lebih lama, Anda memiliki tiga opsi.
Opsi 1: Terima pengurangan performa
Jangan buat perubahan pada tugas Anda. MSBuild menjalankan tugas yang tidak diatribusikan dalam proses anak perusahaan TaskHost , yang lebih lambat tetapi kompatibel sepenuhnya. Opsi ini tidak memerlukan perubahan kode.
Opsi 2: Pertahankan implementasi terpisah
Bangun rakitan tugas terpisah untuk MSBuild 18.6+ dan versi yang lebih lama. Versi MSBuild 18.6+ mengimplementasikan IMultiThreadableTask dan menggunakan TaskEnvironment. Versi yang lebih lama masih menggunakan Task dengan API tingkat proses.
Opsi 3: Jembatan kompatibilitas
Tentukan MSBuildMultiThreadableTaskAttribute sendiri dalam susunan tugas Anda. Karena MSBuild mendeteksi atribut hanya berdasarkan namespace dan nama (mengabaikan assembly yang mendefinisikannya), atribut yang Anda definisikan sendiri berfungsi baik pada MSBuild versi lama maupun baru:
namespace Microsoft.Build.Framework
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}
Saat berjalan di MSBuild 18.6 atau yang lebih baru, MSBuild mengenali atribut dan menjalankan tugas dalam proses. Saat berjalan pada versi sebelumnya, MSBuild mengabaikan atribut yang tidak diketahui dan menjalankan tugas seperti sebelumnya.
Dengan opsi ini, Anda tidak memiliki akses ke TaskEnvironment, sehingga Anda harus menangani semua yang ditanganinya secara manual, seperti mengonversi semua jalur relatif Anda ke jalur absolut.
Perbandingan pendekatan
Tabel berikut membandingkan tiga pendekatan saat berjalan dalam mode multithreaded (-mt). Dalam mode non-multithread, semua tugas berjalan di luar proses, terlepas dari bagaimana tugas tersebut ditandai.
| Approach | Maintenance | Performa (18.6+) | Kinerja (lama) | Akses TaskEnvironment |
|---|---|---|---|---|
| Implementasi terpisah | Tinggi | Dalam proses penuh | Sepenuhnya di luar proses | Ya (versi 18.6+) |
| Jembatan kompatibilitas | Rendah | Dalam proses penuh | Sepenuhnya di luar proses | Tidak (hanya atribut) |
| Tidak ada perubahan | None | Sidecar (lebih lambat) | Sepenuhnya di luar proses | No |