Olá a todos. Preparamos uma tradução de outro material útil na véspera do início do curso "Desenvolvedor C #" . Gostar de ler.
Como fiz recentemente uma lista de práticas recomendadas em C # para o Criteo, achei que seria bom compartilhá-lo publicamente. O objetivo deste artigo é fornecer uma lista incompleta de modelos de código que devem ser evitados, porque são questionáveis ou porque funcionam mal. A lista pode parecer um pouco aleatória porque está levemente fora de contexto, mas todos os seus elementos foram encontrados em algum momento do nosso código e causaram problemas na produção. Espero que isso sirva como uma boa prevenção e evite seus erros no futuro.Observe também que os serviços web Criteo dependem de código de alto desempenho, daí a necessidade de evitar códigos ineficientes. Na maioria dos aplicativos, não haverá diferença tangível perceptível na substituição de alguns desses modelos.E por último, mas não menos importante, alguns pontos (por exemplo ConfigureAwait
) já foram discutidos em muitos artigos, por isso não vou me aprofundar neles em detalhes. O objetivo é formar uma lista compacta de pontos aos quais você precisa prestar atenção, e não fornecer um cálculo técnico detalhado para cada um deles.Esperando de forma síncrona por código assíncrono
Nunca espere tarefas inacabadas de forma síncrona. Isto aplica-se, mas não se limita a: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll
.Como uma generalização: qualquer relacionamento síncrono entre dois encadeamentos de conjunto pode causar esgotamento do conjunto. As causas desse fenômeno são descritas neste artigo .ConfigureAwait
Se seu código puder ser chamado de um contexto de sincronização, use ConfigureAwait(false)
para cada uma das chamadas em espera.Observe que isso ConfigureAwait
sóawait
é útil ao usar uma palavra-chave .Por exemplo, o código a seguir não faz sentido:
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();
vazio assíncrono
Nunca useasync void
. A exceção lançada no async void
método se propaga para o contexto de sincronização e geralmente causa o travamento de todo o aplicativo.Se você não puder retornar a tarefa no seu método (por exemplo, porque está implementando a interface), mova o código assíncrono para outro método e chame-o:interface IInterface
{
void DoSomething();
}
class Implementation : IInterface
{
public void DoSomething()
{
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync()
{
await Task.Delay(100);
}
}
Evite async sempre que possível
Por hábito ou por causa da memória muscular, você pode escrever algo como:public async Task CallAsync()
{
var client = new Client();
return await client.GetAsync();
}
Embora o código esteja semanticamente correto, o uso de uma palavra async
- chave não é necessário aqui e pode resultar em sobrecarga significativa em um ambiente altamente carregado. Tente evitá-lo sempre que possível:public Task CallAsync()
{
var client = new Client();
return _client.GetAsync();
}
No entanto, lembre-se de que você não pode recorrer a essa otimização quando seu código estiver agrupado em blocos (por exemplo, try/catch
ou using
):public async Task Correct()
{
using (var client = new Client())
{
return await client.GetAsync();
}
}
public Task Incorrect()
{
using (var client = new Client())
{
return client.GetAsync();
}
}
Na versão errada ( Incorrect()
), o cliente pode ser excluído antes que a GetAsync
chamada seja concluída , pois a tarefa dentro do bloco de uso não é esperada por aguardar.Comparações regionais
Se você não tiver motivos para usar comparações regionais, sempre use comparações ordinais . Embora, devido às otimizações internas, isso realmente não importe para as formas de apresentação de dados nos EUA, a comparação é uma ordem de magnitude mais lenta para as formas de apresentação de outras regiões (e até duas ordens de magnitude no Linux!). Como a comparação de cadeias é uma operação frequente na maioria dos aplicativos, a sobrecarga aumenta significativamente.ConcurrentBag <
T>
Nunca use ConcurrentBag<
T>
sem benchmarking . Essa coleção foi projetada para casos de uso muito específicos (quando na maioria das vezes o item é excluído da fila pelo encadeamento que o enfileirou) e sofre de sérios problemas de desempenho se for usado para outros fins. Se você precisa de uma coleção thread-safe, preferem ConcurrentQueue <
T>
.ReaderWriterLock / ReaderWriterLockSlim <
T >
Nunca use sem benchmarking. ReaderWriterLock<T>
/ReaderWriterLockSlim<T>
Embora o uso desse tipo de primitiva de sincronização especializada ao trabalhar com leitores e gravadores possa ser tentador, seu custo é muito maior que o simples Monitor
(usado com uma palavra-chave lock
). Se o número de leitores que executam a seção crítica ao mesmo tempo não for muito grande, a simultaneidade não será suficiente para absorver o aumento da sobrecarga e o código funcionará pior.
Preferir funções lambda em vez de grupos de métodos
Considere o seguinte código:public IEnumerable<int> GetItems()
{
return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
O Re-compartilhador sugere reescrever o código sem uma função lambda, que pode parecer um pouco mais limpa:public IEnumerable<int> GetItems()
{
return _list.Where(Filter);
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Infelizmente, isso leva à alocação de memória dinâmica para cada chamada. De fato, a chamada é compilada como:public IEnumerable<int> GetItems()
{
return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Isso pode ter um impacto significativo no desempenho se o código for chamado em uma seção muito carregada.O uso das funções lambda inicia a otimização do compilador, que armazena em cache o delegado em um campo estático, evitando a alocação. Isso funciona apenas se Filter
estático. Caso contrário, você pode armazenar em cache o delegado:private Predicate<int> _filter;
public Constructor()
{
_filter = new Predicate<int>(Filter);
}
public IEnumerable<int> GetItems()
{
return _list.Where(_filter);
}
private bool Filter(int element)
{
return i % 2 == 0;
}
Converter enumerações em seqüências de caracteres
Chamando Enum.ToString
em .net
é muito caro, porque a reflexão é usado para converter dentro, e chamar o método virtual na provoca estrutura da embalagem. Isso deve ser evitado o máximo possível.As enumerações geralmente podem ser substituídas por seqüências constantes:
public enum Numbers
{
One,
Two,
Three
}
public static class Numbers
{
public const string One = "One";
public const string Two = "Two";
public const string Three = "Three";
}
Se você realmente precisar usar uma enumeração, considere armazenar em cache o valor convertido em um dicionário para amortizar a sobrecarga.Comparação de enumeração
Nota: isso não é mais relevante no núcleo .net, desde a versão 2.1, a otimização é executada pelo JIT automaticamente.
Ao usar enumerações como sinalizadores, pode ser tentador usar o método Enum.HasFlag
:[Flags]
public enum Options
{
Option1 = 1,
Option2 = 2,
Option3 = 4
}
private Options _option;
public bool IsOption2Enabled()
{
return _option.HasFlag(Options.Option2);
}
Este código provoca dois pacotes com alocação: um para a conversão Options.Option2
para Enum
e outra para uma chamada virtual HasFlag
para a estrutura. Isso torna esse código desproporcionalmente caro. Em vez disso, você deve sacrificar a legibilidade e usar operadores binários:public bool IsOption2Enabled()
{
return (_option & Options.Option2) == Options.Option2;
}
Implementação de métodos de comparação para estruturas
Ao usar uma estrutura em comparações (por exemplo, quando usada como uma chave para um dicionário), você precisa substituir os métodos Equals/GetHashCode
. A implementação padrão usa reflexão e é muito lenta. A implementação gerada pelo Resharper geralmente é muito boa.Você pode saber mais sobre isso aqui: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-cEvite embalagens inadequadas ao usar estruturas com interfaces
Considere o seguinte código:public class IntValue : IValue
{
}
public void DoStuff()
{
var value = new IntValue();
LogValue(value);
SendValue(value);
}
public void SendValue(IValue value)
{
}
public void LogValue(IValue value)
{
}
Tornar IntValue
estruturado pode ser tentador para evitar a alocação de memória dinâmica. Mas desde AddValue
e SendValue
são esperados interface e interfaces têm semântica de referência, o valor será embalado com cada chamada, negando os benefícios deste "otimização". De fato, haverá ainda mais alocações de memória do que se IntValue
fosse uma classe, pois o valor será empacotado independentemente para cada chamada.Se você está escrevendo uma API e espera que alguns valores sejam estruturas, tente usar métodos genéricos:public struct IntValue : IValue
{
}
public void DoStuff()
{
var value = new IntValue();
LogValue(value);
SendValue(value);
}
public void SendValue<T>(T value) where T : IValue
{
}
public void LogValue<T>(T value) where T : IValue
{
}
Embora a conversão desses métodos para universal pareça inútil à primeira vista, na verdade ela permite evitar o empacotamento com alocação no caso em que IntValue
é uma estrutura.CancellationToken Subscriptions Always Inline
Quando você cancela CancellationTokenSource
, todas as assinaturas serão executadas dentro do segmento atual. Isso pode levar a pausas não planejadas ou até a conflitos implícitos.var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel();
Você não pode escapar desse comportamento. Portanto, ao cancelar CancellationTokenSource
, pergunte a si mesmo se você pode permitir que seu segmento atual seja capturado com segurança. Se a resposta for não, passe a chamada para Cancel
dentro Task.Run
para executá-la no conjunto de encadeamentos.Continuações de origem frequentemente alinhadas
Assim como as assinaturas CancellationToken
, as continuações TaskCompletionSource
geralmente são inline. Essa é uma boa otimização, mas pode causar erros implícitos. Por exemplo, considere o seguinte programa:class Program
{
private static ManualResetEventSlim _mutex = new ManualResetEventSlim();
public static async Task Deadlock()
{
await ProcessAsync();
_mutex.Wait();
}
private static Task ProcessAsync()
{
var tcs = new TaskCompletionSource<bool>();
Task.Run(() =>
{
Thread.Sleep(2000);
tcs.SetResult(true);
_mutex.Set();
});
return tcs.Task;
}
static void Main(string[] args)
{
Deadlock().Wait();
Console.WriteLine("Will never get there");
}
}
A chamada tcs.SetResult
faz com que a continuação await ProcessAsync()
seja executada no encadeamento atual. Portanto, a instrução _mutex.Wait()
é executada pelo mesmo encadeamento que deve chamar _mutex.Set()
, o que leva a um impasse. Isso pode ser evitado passando o parâmetro TaskCreationsOptions.RunContinuationsAsynchronously
c TaskCompletionSource
.Se você não tiver um bom motivo para negligenciá-lo, sempre use a opção TaskCreationsOptions.RunContinuationsAsynchronously
ao criar TaskCompletionSource
.Tenha cuidado: o código também irá compilar se você usa TaskContinuationOptions.RunContinuationsAsynchronously
vez TaskCreationOptions.RunContinuationsAsynchronously
, mas os parâmetros serão ignorados, e as continuações ainda será embutido. Esse é um erro surpreendentemente comum porque TaskContinuationOptions
precede o TaskCreationOptions
preenchimento automático.Task.Run / Task.Factory.StartNew
Se você não tiver motivos para usar Task.Factory.StartNew
, sempre opte Task.Run
por executar uma tarefa em segundo plano. Task.Run
usa valores padrão mais seguros e, mais importante, descompacta automaticamente a tarefa retornada, o que pode evitar erros não óbvios com métodos assíncronos. Considere o seguinte programa:class Program
{
public static async Task ProcessAsync()
{
await Task.Delay(2000);
Console.WriteLine("Processing done");
}
static async Task Main(string[] args)
{
await Task.Factory.StartNew(ProcessAsync);
Console.WriteLine("End of program");
Console.ReadLine();
}
}
Apesar de sua aparência, o fim do programa será exibido antes do processamento concluído. Isso ocorre porque ele Task.Factory.StartNew
retornará Task<Task>
e o código espera apenas uma tarefa externa. O código correto pode ser await Task.Factory.StartNew(ProcessAsync).Unwrap()
, ou await Task.Run(ProcessAsync)
.Existem apenas três casos de uso válidos Task.Factory.StartNew
:- Executando uma tarefa em outro planejador.
- Executando uma tarefa em um encadeamento dedicado (usando
TaskCreationOptions.LongRunning
). - (
TaskCreationOptions.PreferFairness
).
.