Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O MSBuild 18.6 apresenta a capacidade de compilar em paralelo dentro do mesmo processo. Para ativar esse modo, use a opção de linha de comando -mt. As versões anteriores do MSBuild tinham suporte para builds paralelos, mas os builds eram feitos em processos separados. Essa alteração tem alguns impactos na forma como você cria tarefas. Enquanto anteriormente, as tarefas eram executadas em um processo separado, agora todas as tarefas habilitadas para multithread são executadas no mesmo processo. Embora a maioria da lógica não precise mudar, há alguns constructos no nível do processo que precisam ser tratados com mais cuidado. Os constructos no nível do processo incluem o diretório de trabalho atual, as variáveis de ambiente e as informações de início do processo (ProcessStartInfo).
Para dar suporte a essas alterações, o MSBuild 18.6 apresenta a interface IMultiThreadableTask (em Microsoft.Build.Framework) e a classe TaskEnvironment.
TaskEnvironmentinclui uma ProjectDirectory propriedade e métodos como GetAbsolutePath(), GetEnvironmentVariable()e SetEnvironmentVariable()GetProcessStartInfo().
Importante
No momento, o modo multithreaded está disponível como um recurso experimental; não é recomendado para uso em produção no momento. Atualizar as dependências da biblioteca do MSBuild para usar as APIs de modo multithreaded impede implicitamente que suas bibliotecas sejam executadas em versões mais antigas do Visual Studio e do MSBuild. Incentivamos os primeiros adotantes a experimentar o modo multithreaded e fornecer comentários. Envie problemas no repositório MSBuild GitHub.
A IMultiThreadableTask interface define o contrato para tarefas que podem ser executadas em processo em builds multithreaded:
// Microsoft.Build.Framework
public interface IMultiThreadableTask : ITask
{
TaskEnvironment TaskEnvironment { get; set; }
}
Para migrar uma tarefa, implemente IMultiThreadableTask junto com sua classe base existente Task e exponha a TaskEnvironment propriedade:
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;
// ...
}
Tarefas que implementam IMultiThreadableTask podem ser executadas em processo. Todas essas tarefas também devem carregar o [MSBuildMultiThreadableTask] atributo, que é o marcador que o MSBuild usa para optar pela tarefa na execução em processo. Antes de adicionar o atributo, confirme se a tarefa não tem nenhuma dependência em constructos de nível de processo, como o diretório de trabalho atual ou o ambiente, e se seu código é thread-safe. Preste atenção especial para garantir o acesso thread-safe a variáveis estáticas, pois essas variáveis são compartilhadas entre todas as instâncias de tarefa e podem ser acessadas ou modificadas por instâncias diferentes da tarefa que também estão em execução no mesmo processo.
Tarefa de exemplo: BuildCommentTask
O exemplo AddBuildCommentTask a seguir é usado ao longo deste artigo para ilustrar o processo de migração. Essa tarefa prepara um comentário de build para arquivos de texto. Por padrão, ele grava texto sem formatação; as propriedades opcionais CommentPrefix e CommentSuffix permitem que os chamadores encapsulam o comentário na sintaxe apropriada ao idioma (por exemplo, // para C#, <!-- e --> para XML, # para Python ou 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;
}
}
}
Um arquivo de projeto pode invocar essa tarefa para diferentes tipos de arquivo, passando a sintaxe de comentário apropriada para cada um:
<!-- 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=" -->" />
Essa tarefa tem quatro problemas de segurança de thread que precisam ser corrigidos em compilações multithread:
-
Caminhos relativos:
File.ReadAllLineseFile.WriteAllLinesuseitem.ItemSpecdiretamente, o que pode ser um caminho relativo. No modo multithreaded, o diretório de trabalho do processo não tem garantia de ser o diretório do projeto. -
Campo estático:
ModifiedFileCounté umstaticcampo compartilhado em todas as instâncias, o que causa corridas de dados quando vários builds são executados simultaneamente. -
Variáveis de ambiente: o problema mais comum com variáveis de ambiente em compilações multithread é o de tarefas que definem variáveis de ambiente antes de criar um processo filho, esperando que ele as herde. No modo multithread,
Environment.SetEnvironmentVariable()modifica o ambiente no nível do processo compartilhado por todas as compilações simultâneas, de modo que uma alteração destinada ao processo filho de um projeto pode acabar afetando o de outro. Ler variáveis de ambiente diretamente no código da tarefa (Environment.GetEnvironmentVariable()) também geralmente é uma má prática; As propriedades do MSBuild são uma alternativa melhor porque são registradas e rastreáveis.
Importante
O modo de compilação multithread está atualmente disponível apenas para compilações da CLI (dotnet build e MSBuild.exe). As compilações do MSBuild no Visual Studio ainda não oferecem suporte à execução multithread no processo. No Visual Studio, toda a execução de tarefas continua sendo executada fora do processo. Visual Studio integração está planejada para uma versão futura.
Pré-requisitos
MSBuild 18.6 ou posterior.
Habilite a execução de tarefas multithreaded com o
-mtcomutador de linha de comando:dotnet build -mtPara obter mais informações sobre a opção
-mt, consulte Referência de linha de comando do MSBuild.
Planejar a migração
Revise o código da tarefa em relação aos seguintes problemas:
- Verifique o código da tarefa e identifique qualquer uso de caminhos relativos. Verifique todas as entradas e as operações de entrada/saída de arquivos.
- Verifique se há usos de variáveis de ambiente.
- Verifique se há uso de
ProcessStartInfoAPI. - Verifique quaisquer campos estáticos ou estruturas de dados e use métodos padrão para torná-los thread-safe.
- Se nenhuma das opções acima se aplicar, considere adicionar somente o atributo.
- Considere requisitos especiais para dar suporte a versões anteriores do MSBuild. Consulte suporte para versões anteriores do MSBuild.
Referência rápida para substituição de API
A tabela a seguir resume as APIs de .NET que você deve substituir e seus equivalentes TaskEnvironment:
| .NET API a ser evitada | Nível | Substituição |
|---|---|---|
Path.GetFullPath(path) |
ERRO | Ver nota a seguir a esta tabela |
File.* com caminhos relativos |
ERRO | Resolva primeiro com TaskEnvironment.GetAbsolutePath() |
Directory.* com caminhos relativos |
ERRO | Resolva primeiro com TaskEnvironment.GetAbsolutePath() |
Environment.GetEnvironmentVariable() |
ERRO | TaskEnvironment.GetEnvironmentVariable() |
Environment.SetEnvironmentVariable() |
ERRO | TaskEnvironment.SetEnvironmentVariable() |
Environment.CurrentDirectory |
ERRO | TaskEnvironment.ProjectDirectory |
new ProcessStartInfo() |
ERRO | TaskEnvironment.GetProcessStartInfo() |
Process.Start() |
ERRO | Utilizar ToolTask ou TaskEnvironment.GetProcessStartInfo() |
| Campos estáticos | AVISO | Use campos de instância ou coleções seguras para threads |
Note
Path.GetFullPath(path) faz duas coisas: converte um caminho relativo em um caminho absoluto e produz uma forma canônica do caminho (resolução . e .. segmentos). Eles precisam ser tratados separadamente:
-
Somente caminho absoluto: Use
TaskEnvironment.GetAbsolutePath(path). Essa abordagem é suficiente para a maioria das operações de entrada/saída de arquivos nas quais você passa o caminho diretamente às APIs do .NET. -
Caminho canônico: se você depender do formulário canônico (por exemplo, ao usar um caminho como um cache ou chave de dicionário), use
Path.GetFullPath(TaskEnvironment.GetAbsolutePath(path))para obter um caminho absoluto totalmente resolvido e canônico.
Marcar a tarefa com o atributo
Todas as tarefas que participam de compilações multithread devem ser marcadas com o atributo [MSBuildMultiThreadableTask]. Esse atributo é o sinal que o MSBuild usa para identificar tarefas seguras para execução em processo.
[MSBuildMultiThreadableTask]
public class MyTask : Task
{
public override bool Execute()
{
// Task logic that doesn't depend on process-level state
return true;
}
}
Se sua tarefa já for thread-safe e não usar nenhuma API de nível de processo (diretório de trabalho atual, variáveis de ambiente, ProcessStartInfo), esse atributo por si só é tudo de que você precisa. A tarefa continua herdando de Task (ou ToolTask) sem nenhuma outra alteração.
Se sua tarefa precisar substituir chamadas à API no nível do processo (por exemplo, para resolver caminhos relativos ou ler variáveis de ambiente com segurança), também implemente IMultiThreadableTask. Essa interface dá à sua tarefa acesso à propriedade TaskEnvironment. O atributo permanece necessário em ambos os casos; IMultiThreadableTask é uma etapa adicional que desbloqueia o TaskEnvironment API.
Note
O MSBuild detecta o MSBuildMultiThreadableTaskAttribute apenas pelo namespace e pelo nome, ignorando o assembly em que ele é definido. Isso significa que você pode definir o atributo por conta própria em seu próprio código (consulte Suporte a versões anteriores do MSBuild) e o MSBuild ainda o reconhece.
Note
O MSBuildMultiThreadableTaskAttribute é não herdável (Inherited = false). Cada classe de tarefa deve declarar explicitamente o atributo para ser reconhecido como multithreadable. Herdar de uma classe que tem o atributo não torna automaticamente a classe derivada multithreadable.
Inicializar TaskEnvironment para Fallback
Ao implementar IMultiThreadableTask, inicialize a TaskEnvironment propriedade para TaskEnvironment.Fallback:
public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;
O MSBuild define essa propriedade antes de chamar Execute() em uma compilação normal. O Fallback padrão garante que a tarefa funcione corretamente em outros cenários de hospedagem (como testes de unidade ou ferramentas de orquestração de build personalizadas) em que o MSBuild não está presente para definir a propriedade. Sem ele, acessar TaskEnvironment fora do mecanismo resultaria em uma exceção de referência nula.
Se você precisar dar suporte a versões do MSBuild anteriores à 18.6 que não incluem TaskEnvironment.Fallback, inicialize a propriedade para null , em vez disso, e proteja todas TaskEnvironment as chamadas com uma verificação nula. Consulte Suporte a versões anteriores do MSBuild para obter mais opções.
Atualizar caminhos e E/S de arquivo
Uma tarefa geralmente aceita entradas, como listas de itens no MSBuild, que se forem arquivos, podem estar na forma de caminhos relativos.
Caminhos relativos são sempre relativos ao diretório de trabalho atual do processo, mas como a tarefa agora é executada em processo, o diretório de trabalho pode não ser o mesmo que era quando a tarefa foi executada em seu próprio processo. Esses caminhos são relativos ao diretório do projeto. O TaskEnvironment inclui a propriedade ProjectDirectory e o método GetAbsolutePath(), que você pode usar para converter caminhos relativos em caminhos absolutos. Você também pode acessar o metadado FullPath; não é necessário usar o caminho relativo ItemSpec e depois convertê-lo em caminho absoluto.
O tipo AbsolutePath
AbsolutePath é um struct readonly em Microsoft.Build.Framework que representa um caminho de arquivo absoluto validado. Os principais membros incluem:
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);
}
O AbsolutePath construtor valida que o caminho fornecido está enraizado. Você também pode construir um AbsolutePath fornecendo um caminho relativo e um caminho base. A conversão implícita para string significa que você pode passar um AbsolutePath diretamente para qualquer API que espere um caminho string.
A OriginalValue propriedade preserva a cadeia de caracteres de caminho original como ela foi passada antes da resolução. Essa propriedade é útil quando você precisa manter caminhos relativos em saídas de tarefa ou mensagens de log. Por exemplo, uma tarefa que registra quais arquivos processou pode usar OriginalValue em suas mensagens de log para que os caminhos na saída permaneçam relativos e legíveis, ao mesmo tempo que ainda usa o Value resolvido (ou a conversão implícita de string) para as operações reais de E/S de arquivo.
Use TaskEnvironment.GetAbsolutePath() para resolver caminhos de itens:
Antes:
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));
Depois:
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}");
Lidar com a contenção de arquivos em compilações paralelas
A contenção de arquivo pode ocorrer sempre que várias tarefas são executadas em paralelo e acessam o mesmo arquivo. Essa preocupação se aplica ao modelo multiprocesso tradicional e ao modo multithreaded mais recente em processo. Em ambos os casos, o mesmo arquivo pode ser acessado simultaneamente quando:
- O mesmo arquivo aparece em vários builds de subprojeto (por exemplo, um arquivo de configuração compartilhado ou um arquivo de origem vinculado).
- Uma tarefa lê e grava um arquivo que outra instância de tarefa também está processando.
Métodos de conveniência como File.ReadAllLines e File.WriteAllLines não fornecem controle explícito sobre o bloqueio de arquivos. Quando o acesso simultâneo for possível, use FileStream com compartilhamento e bloqueio explícitos:
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);
}
Diretrizes principais para e/S de arquivo em tarefas multithreaded:
- Use
FileShare.Nonepara operações de leitura-modificação-gravação. Essa configuração impede que outra tarefa leia conteúdo obsoleto enquanto você atualiza o arquivo. - Pegue
IOExceptione considere tentar novamente. Quando outra tarefa ou processo detém um bloqueio, sua tentativa de abertura lançaIOException. Uma pequena repetição com retirada geralmente é apropriada. - Evite manter bloqueios em vários arquivos ao mesmo tempo. Se duas tarefas bloquearem um arquivo e tentarem bloquear o outro, você obterá um deadlock. Se você precisar operar em vários arquivos, bloqueie-os em uma ordem consistente (por exemplo, classificada por caminho completo).
- Mantenha os bloqueios o mais curtos possível. Abra o arquivo, leia, modifique, escreva e feche em uma operação. Não mantenha um bloqueio de arquivo durante o trabalho não relacionado.
O exemplo anterior é uma abordagem. Para obter orientações gerais sobre operações de entrada e saída (E/S) de arquivo seguras para threads no .NET, consulte classe FileStream, enumeração FileShare e Práticas recomendadas de threading gerenciado.
Note
TaskEnvironment em si não é thread-safe. Isso só importa se sua tarefa gera internamente seus próprios threads (por exemplo, usando Parallel.ForEach ou Task.Run). A maioria das tarefas não faz isso. Eles implementam Execute() de forma linear e deixam o MSBuild gerenciar o paralelismo entre instâncias de tarefa. Se sua tarefa criar suas próprias threads, capture os valores de TaskEnvironment em variáveis locais antes de gerá-las, em vez de acessar TaskEnvironment simultaneamente de várias threads.
Atualizar variáveis de ambiente
Note
Ler variáveis de ambiente no código da tarefa geralmente é uma má prática, mesmo em builds de thread único. As propriedades do MSBuild são uma alternativa melhor: têm escopo definido explicitamente, são registradas durante a compilação e podem ser rastreadas no log de compilação. Se sua tarefa atualmente lê uma variável de ambiente para receber dados de entrada, considere substituí-la por uma propriedade da tarefa. O projeto ainda pode derivar o valor de uma variável de ambiente: <AddBuildCommentTask DisableComments="$(DISABLE_BUILD_COMMENTS)" ... />.
A orientação nesta seção é migrar tarefas existentes que já dependem de variáveis de ambiente. Se você tiver a oportunidade de refatorar, prefira propriedades e itens.
Configurando variáveis de ambiente para processos filho
O problema de variável de ambiente mais comum em builds multithread é uma tarefa que define uma variável de ambiente e, em seguida, gera um processo filho, esperando que o filho o herda. No modelo de vários processos, Environment.SetEnvironmentVariable() modificou com segurança o ambiente de processo de trabalho para esse projeto. No modo multithreaded, o processo é compartilhado em todos os builds simultâneos, portanto, uma alteração destinada ao processo filho de um projeto pode vazar para outro.
Use TaskEnvironment.SetEnvironmentVariable() em conjunto com TaskEnvironment.GetProcessStartInfo() (consulte Atualizar chamadas de API do ProcessStart).
GetProcessStartInfo() retorna um ProcessStartInfo já preenchido com o diretório de trabalho do projeto e sua tabela de ambiente isolada, incluindo todas as variáveis que você definiu com SetEnvironmentVariable(), para que os processos-filhos herdem automaticamente o ambiente correto, restrito ao projeto.
Antes:
Environment.SetEnvironmentVariable("TOOL_OUTPUT_DIR", outputDir);
var startInfo = new ProcessStartInfo("mytool.exe") { UseShellExecute = false };
Process.Start(startInfo); // inherits the modified process-level environment
Depois:
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
Lendo variáveis de ambiente em tarefas existentes
Se a tarefa existente lê variáveis de ambiente e você não puder refatorar imediatamente para as propriedades da tarefa, substitua Environment.GetEnvironmentVariable() por TaskEnvironment.GetEnvironmentVariable(). Essa chamada de método lê da tabela de variáveis de ambiente no escopo do projeto, em vez do ambiente compartilhado do processo, de modo que compilações simultâneas não interfiram entre si.
Antes (de BuildCommentTask):
string disableComments = Environment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Depois:
string disableComments = TaskEnvironment.GetEnvironmentVariable("DISABLE_BUILD_COMMENTS");
Dica
Ao atualizar o código existente que lê uma variável de ambiente, considere substituir o padrão por uma propriedade de tarefa. Por exemplo, exponha public bool DisableComments { get; set; } na tarefa e deixe o projeto passar DisableComments="$(DISABLE_BUILD_COMMENTS)". O MSBuild registra o valor resolvido, tornando-o visível no log de build e muito mais fácil de diagnosticar do que uma variável de ambiente oculta lida.
Atualizar chamadas à API do ProcessStart
Normalmente, se uma tarefa inicia um processo, você deve usar ToolTask, que cuida de tudo para você. Nos casos em que você estiver atualizando uma tarefa que chama ProcessStartInfo diretamente, use TaskEnvironment.GetProcessStartInfo(). Isso retorna um ProcessStartInfo configurado com o diretório de trabalho do projeto e sua tabela de ambiente isolada. Se você também estiver definindo variáveis de ambiente antes de iniciar, use TaskEnvironment.SetEnvironmentVariable() primeiro, conforme mostrado na seção anterior.
Antes:
var startInfo = new ProcessStartInfo("mytool.exe")
{
WorkingDirectory = ".",
UseShellExecute = false
};
Process.Start(startInfo);
Depois:
ProcessStartInfo startInfo = TaskEnvironment.GetProcessStartInfo();
startInfo.FileName = "mytool.exe";
startInfo.UseShellExecute = false;
Process.Start(startInfo);
Note
Se sua tarefa herda de ToolTask, as informações de inicialização do processo já são tratadas para você. Você só precisa atualizar as tarefas que criam ProcessStartInfo diretamente.
Atualizar campos estáticos e estruturas de dados para serem thread-safe
Campos estáticos exigem tratamento cuidadoso ao migrar para builds multithreaded. Mesmo no modelo de vários processos, um único processo pode criar vários projetos, de modo que o estado estático seja compartilhado, mas não simultaneamente.
O modo multithreaded adiciona uma nova dimensão a esse problema. Várias compilações agora podem compartilhar o mesmo processo e executar tarefas concorrentemente (especialmente com o MSBuild Server, que é habilitado automaticamente com multithread). Um campo estático é compartilhado por todas as instâncias de tarefa no processo, não apenas na sua compilação, mas potencialmente entre invocações separadas de compilação executadas simultaneamente. Por exemplo, dois desenvolvedores em execução dotnet build ao mesmo tempo em um servidor de build ou duas janelas de terminal no mesmo computador podem compartilhar o mesmo estado estático e agora esses builds o acessam ao mesmo tempo.
BuildCommentTask No exemplo, o campo ModifiedFileCount estático é compartilhado em todas as instâncias:
Antes:
private static int ModifiedFileCount = 0;
// In Execute():
ModifiedFileCount++;
Esse código tem dois problemas. Primeiro, o ++ operador não é atômico. Quando várias instâncias de tarefa são executadas concorrentemente, duas threads podem ler o mesmo valor e ambas gravar o mesmo resultado incrementado, causando perda de contagens. Em segundo lugar, como o campo é estático, ele persiste entre compilações e é compartilhado entre compilações concorrentes no mesmo processo.
As seções a seguir mostram duas abordagens para corrigir esses problemas, da mais simples à mais correta.
Abordagem 1: usar uma API segura para threads, mas válida para todo o processo
A correção mais simples é tornar o incremento atômico:
private static int ModifiedFileCount = 0;
// In Execute():
int fileNumber = Interlocked.Increment(ref ModifiedFileCount);
Interlocked.Increment executa a leitura-incremento-gravação como uma única operação atômica, portanto nenhuma contagem é perdida. Essa abordagem resolve o problema de concorrência, mas o contador ainda é compartilhado entre todas as compilações no processo, incluindo compilações consecutivas e concorrentes. Se dois builds forem executados simultaneamente, os números de seus arquivos serão intercalados (Build A recebe #1, #3, #5; Build B recebe #2, #4, #6). Se essa situação é aceitável depende se sua tarefa requer isolamento por build. Para um contador de numeração de arquivo sequencial, como ModifiedFileCount, o compartilhamento entre builds é um problema de correção; use RegisterTaskObject em vez disso (consulte a Abordagem 2).
Aqui, o equivalente de API thread-safe, porém válido para todo o processo, é InterlockedIncrement, mas, no seu próprio código, você precisaria encontrar substitutos thread-safe adequados para quaisquer APIs que não sejam thread-safe. Por exemplo, se sua tarefa mantiver o estado usando um Dictionary, considere usar ConcurrentDictionary<TKey,TValue>.
Abordagem 2: RegisterTaskObject para isolamento no escopo da compilação
Se sua tarefa precisar de um estado estático compartilhado por subprojetos durante uma única invocação de build, mas isolado de outras compilações simultâneas, use IBuildEngine4.RegisterTaskObject com RegisteredTaskObjectLifetime.Build. O MSBuild gerencia o tempo de vida do objeto, que é criado no primeiro uso e limpo quando o build termina. Observe que os objetos registrados precisam ser thread-safe.
Primeiro, defina uma classe simples de contador thread-safe:
internal class FileCounter
{
private int _count = 0;
public int Next() => Interlocked.Increment(ref _count);
}
Em seguida, use um método auxiliar com bloqueio de verificação dupla para obter ou criar o contador:
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;
}
Em Execute():
FileCounter counter = GetOrCreateCounter();
// ...
int fileNumber = counter.Next();
Com essa abordagem, cada invocação de build obtém sua própria FileCounter. Todos os subprojetos no mesmo build compartilham o contador (numeração sequencial), mas uma execução separada dotnet build ao mesmo tempo no mesmo computador obtém um contador diferente.
RegisteredTaskObjectLifetime.Build instrui o MSBuild a limitar o escopo do objeto à invocação de compilação atual e limpá-lo quando a compilação terminar.
Escolher a abordagem certa
Ao decidir como lidar com o estado estático, comece com essa pergunta: esses dados são seguros para compartilhar em todos os builds que podem ser executados no mesmo processo, incluindo builds consecutivos e builds simultâneos?
Os processos de trabalho do MSBuild persistem entre invocações (a reutilização de nós é ativada por padrão), e um processo do MSBuild pode potencialmente atender a várias compilações de solução ao longo do seu ciclo de vida, não apenas em uma única chamada dotnet build. Não suponha que um processo manipule apenas um build.
Siga estas diretrizes:
- Mantenha o campo estático somente se os dados armazenados em cache forem seguros para acessar de vários threads em diferentes projetos e em vários builds sem a necessidade de invalidação entre builds. Por exemplo, um cache de dados imutáveis calculados uma única vez a partir de entradas que nunca mudam (como metadados de assembly carregados uma única vez durante a inicialização) pode se enquadrar.
-
Use
IBuildEngine4.RegisterTaskObjectcomRegisteredTaskObjectLifetime.Buildquando o estado precisar ser isolado a cada invocação de build (por exemplo, contadores, acumuladores ou caches que devem ser redefinidos entre builds ou não vazar entre builds executados simultaneamente). Esta é a abordagem preferida para a maioria dos estados mutáveis compartilhados. -
Use os primitivos
System.Threading(Interlocked,ConcurrentDictionary,lock,ReaderWriterLockSlim) para tornar thread-safe qualquer estado estático retido, mas lembre-se de que a segurança de threads, por si só, não fornece isolamento no nível da build. Consulte as práticas recomendadas para threads gerenciadas.
Dica
O exemplo completo de migração, mais adiante neste artigo, usa a abordagem RegisterTaskObject para demonstrar o isolamento no escopo do build.
Exemplo de migração completa
O código a seguir mostra a migração AddBuildCommentTask completa com todas as cinco alterações aplicadas:
- Tem o atributo
[MSBuildMultiThreadableTask], marcando-o para execução no processo. - Implementa
IMultiThreadableTask, além da classe base existenteTask, e expõe a propriedadeTaskEnvironment. - Usa
TaskEnvironment.GetAbsolutePath()para resolução de caminho. - Usa
TaskEnvironment.GetEnvironmentVariable()em vez deEnvironment.GetEnvironmentVariable(). - Usa
IBuildEngine4.RegisterTaskObjectcomRegisteredTaskObjectLifetime.Buildpara limitar o contador de arquivos à invocação atual da compilação, substituindo o contador estático de todo o processo.
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;
}
}
}
O que acontece com tarefas não migradas
As tarefas que não têm o [MSBuildMultiThreadableTask] atributo ou não implementam IMultiThreadableTask continuam funcionando sem alterações. O MSBuild executa essas tarefas em um processo subsidiária TaskHost , que fornece o mesmo isolamento em nível de processo que as versões anteriores do MSBuild. Essa abordagem é mais lenta devido à sobrecarga da comunicação entre processos, mas é totalmente compatível com o código de tarefa existente. A migração é opcional para correção — tarefas não migradas ainda produzem resultados corretos— mas a migração melhora o desempenho do build.
Suporte a versões anteriores do MSBuild
Se você atualizar sua tarefa personalizada e distribuí-la para outras pessoas, sua tarefa dará suporte a clientes usando o MSBuild 18.6 ou posterior. Para dar suporte a clientes em versões anteriores do MSBuild, você tem três opções.
Opção 1: Aceitar desempenho reduzido
Não faça alterações em sua tarefa. O MSBuild executa tarefas não atribuídas em um processo de subsidiária TaskHost , que é mais lento, mas totalmente compatível. Essa opção não requer alterações de código.
Opção 2: manter implementações separadas
Crie assemblies de tarefas separados para o MSBuild 18.6+ e versões anteriores. A versão do MSBuild 18.6+ implementa IMultiThreadableTask e usa TaskEnvironment. A versão anterior continua usando Task com APIs em nível de processo.
Opção 3: ponte de compatibilidade
Defina você mesmo o MSBuildMultiThreadableTaskAttribute no assembly da tarefa. Como o MSBuild detecta o atributo somente por namespace e nome (ignorando o assembly definidor), seu atributo autodefinido funciona em versões antigas e novas do MSBuild:
namespace Microsoft.Build.Framework
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
internal class MSBuildMultiThreadableTaskAttribute : Attribute { }
}
Ao executar no MSBuild 18.6 ou posterior, o MSBuild reconhece o atributo e executa a tarefa em processo. Ao executar em versões anteriores, o MSBuild ignora o atributo desconhecido e executa a tarefa como antes.
Com essa opção, você não tem acesso a TaskEnvironment, portanto terá de lidar manualmente com tudo o que ela faz, como converter todos os caminhos relativos em absolutos.
Comparação de abordagens
A tabela a seguir compara as três abordagens ao executar no modo multithreaded (-mt). No modo não multithreaded, todas as tarefas são executadas fora do processo, independentemente de como são marcadas.
| Abordagem | Maintenance | Desempenho (18,6+) | Desempenho (mais antigo) | Acesso a TaskEnvironment |
|---|---|---|---|---|
| Implementações separadas | Alta | Totalmente em processamento | Totalmente fora de processo | Sim (versão 18.6+) |
| Ponte de compatibilidade | Baixo | Totalmente em processamento | Totalmente fora do processo | Não (apenas atributo) |
| Nenhuma alteração | None | Sidecar (mais lento) | Totalmente fora do processo | No |