Bonjour à tous. Nous avons préparé une traduction d'un autre matériel utile à la veille du début du cours "Développeur C #" . Bonne lecture.
Depuis que j'ai récemment fait une liste des meilleures pratiques en C # pour Criteo, j'ai pensé que ce serait bien de la partager publiquement. Le but de cet article est de fournir une liste incomplète de modèles de code à éviter, soit parce qu'ils sont discutables, soit parce qu'ils fonctionnent mal. La liste peut sembler un peu aléatoire, car elle est légèrement hors contexte, mais tous ses éléments ont été trouvés dans notre code à un moment donné et ont causé des problèmes de production. J'espère que cela servira de bonne prévention et évitera vos erreurs à l'avenir.Notez également que les services Web Criteo reposent sur du code haute performance, d'où la nécessité d'éviter un code inefficace. Dans la plupart des applications, il n'y aura pas de différence tangible notable par rapport au remplacement de certains de ces modèles.Enfin et surtout, certains points (par exemple ConfigureAwait
) ont déjà été abordés dans de nombreux articles, je ne m'étendrai donc pas sur eux en détail. L'objectif est de former une liste compacte de points auxquels vous devez prêter attention, et non de donner un calcul technique détaillé pour chacun d'eux.Attente synchrone de code asynchrone
Ne vous attendez jamais à des tâches inachevées de manière synchrone. Cela vaut, mais ne se limite pas à: Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll
.En général: toute relation synchrone entre deux threads de pool peut entraîner l'épuisement du pool. Les causes de ce phénomène sont décrites dans cet article .ConfigureAwait
Si votre code peut être appelé à partir d'un contexte de synchronisation, utilisez-le ConfigureAwait(false)
pour chacun de vos appels en attente.Veuillez noter que cela n'est ConfigureAwait
utile que lorsque vous utilisez un mot clé await
.Par exemple, le code suivant n'a pas de sens:
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();
async void
Ne jamais utiliserasync void
. L'exception levée dans la async void
méthode se propage au contexte de synchronisation et provoque généralement le blocage de l'application entière.Si vous ne pouvez pas renvoyer la tâche dans votre méthode (par exemple, parce que vous implémentez l'interface), déplacez le code asynchrone vers une autre méthode et appelez-la:interface IInterface
{
void DoSomething();
}
class Implementation : IInterface
{
public void DoSomething()
{
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync()
{
await Task.Delay(100);
}
}
Évitez l'async autant que possible
Par habitude ou à cause de la mémoire musculaire, vous pouvez écrire quelque chose comme:public async Task CallAsync()
{
var client = new Client();
return await client.GetAsync();
}
Bien que le code soit sémantiquement correct, l'utilisation d'un mot clé async
n'est pas nécessaire ici et peut entraîner une surcharge importante dans un environnement très chargé. Essayez de l'éviter autant que possible:public Task CallAsync()
{
var client = new Client();
return _client.GetAsync();
}
Cependant, gardez à l'esprit que vous ne pouvez pas recourir à cette optimisation lorsque votre code est encapsulé dans des blocs (par exemple, 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();
}
}
Dans la mauvaise version ( Incorrect()
), le client peut être supprimé avant la fin de l' GetAsync
appel, car la tâche à l'intérieur du bloc using n'est pas attendue par wait.Comparaisons régionales
Si vous n'avez aucune raison d'utiliser des comparaisons régionales, utilisez toujours des comparaisons ordinales . Bien qu'en raison des optimisations internes, cela n'a pas beaucoup d'importance pour les formulaires de présentation des données en-US, la comparaison est un ordre de grandeur plus lent pour les formulaires de présentation des autres régions (et jusqu'à deux ordres de grandeur sur Linux!). Étant donné que la comparaison de chaînes est une opération fréquente dans la plupart des applications, la surcharge augmente considérablement.ConcurrentBag <
T>
Ne jamais utiliser ConcurrentBag<
T>
sans analyse comparative . Cette collection a été conçue pour des cas d'utilisation très spécifiques (lorsque la plupart du temps l'élément est exclu de la file d'attente par le thread qui l'a mis en file d'attente) et souffre de graves problèmes de performances s'il est utilisé à d'autres fins. Si vous avez besoin d' une collection thread-safe, préfèrent ConcurrentQueue <
T>
.ReaderWriterLock / ReaderWriterLockSlim <
T >
Ne jamais utiliser sans analyse comparative. ReaderWriterLock<T>
/ReaderWriterLockSlim<T>
Bien que l'utilisation de ce type de primitive de synchronisation spécialisée lorsque vous travaillez avec des lecteurs et des écrivains puisse être tentante, son coût est beaucoup plus élevé que le simple Monitor
(utilisé avec le mot-clé lock
). Si le nombre de lecteurs exécutant la section critique en même temps n'est pas très élevé, la simultanéité ne sera pas suffisante pour absorber la surcharge accrue et le code fonctionnera moins bien.
Préférez les fonctions lambda au lieu des groupes de méthodes
Considérez le code suivant:public IEnumerable<int> GetItems()
{
return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Resharper suggère de réécrire du code sans fonction lambda, ce qui pourrait sembler un peu plus propre:public IEnumerable<int> GetItems()
{
return _list.Where(Filter);
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Malheureusement, cela conduit à l'allocation de mémoire dynamique pour chaque appel. En fait, l'appel se compile comme:public IEnumerable<int> GetItems()
{
return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Cela peut avoir un impact significatif sur les performances si le code est appelé dans une section fortement chargée.L'utilisation des fonctions lambda démarre l'optimisation du compilateur, qui met en cache le délégué dans un champ statique, en évitant l'allocation. Cela ne fonctionne que si Filter
statique. Sinon, vous pouvez mettre en cache le délégué vous-même: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;
}
Convertir les énumérations en chaînes
L' appel Enum.ToString
en .net
est assez cher, parce que la réflexion est utilisé pour convertir l' intérieur, et appelant la méthode virtuelle sur la structure provoque emballage. Cela devrait être évité autant que possible.Les énumérations peuvent souvent être remplacées par des chaînes 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";
}
Si vous devez vraiment utiliser une énumération, pensez à mettre en cache la valeur convertie dans un dictionnaire pour amortir les frais généraux.Comparaison d'énumération
Remarque: cela n'est plus pertinent dans le noyau .net, depuis la version 2.1, l'optimisation est effectuée automatiquement par JIT.
Lorsque vous utilisez des énumérations comme indicateurs, il peut être tentant d'utiliser la méthode Enum.HasFlag
:[Flags]
public enum Options
{
Option1 = 1,
Option2 = 2,
Option3 = 4
}
private Options _option;
public bool IsOption2Enabled()
{
return _option.HasFlag(Options.Option2);
}
Ce code provoque deux packages avec allocation: un pour la conversion Options.Option2
en Enum
, et l'autre pour un appel virtuel HasFlag
pour la structure. Cela rend ce code excessivement cher. Au lieu de cela, vous devez sacrifier la lisibilité et utiliser des opérateurs binaires:public bool IsOption2Enabled()
{
return (_option & Options.Option2) == Options.Option2;
}
Mise en place de méthodes de comparaison des structures
Lorsque vous utilisez une structure dans des comparaisons (par exemple, lorsqu'elle est utilisée comme clé pour un dictionnaire), vous devez remplacer les méthodes Equals/GetHashCode
. L'implémentation par défaut utilise la réflexion et est très lente. L'implémentation générée par Resharper est généralement assez bonne.Vous pouvez en savoir plus à ce sujet ici: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-cÉvitez les emballages inappropriés lorsque vous utilisez des structures avec des interfaces
Considérez le code suivant: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)
{
}
Rendre IntValue
structuré peut être tentant d'éviter d'allouer de la mémoire dynamique. Mais puisque l' interface est AddValue
et SendValue
sont attendues, et que les interfaces ont une sémantique de référence, la valeur sera emballée à chaque appel, annulant les avantages de cette "optimisation". En fait, il y aura encore plus d'allocations de mémoire que s'il IntValue
s'agissait d'une classe, car la valeur sera mise en package indépendamment pour chaque appel.Si vous écrivez une API et que certaines valeurs sont des structures, essayez d'utiliser des méthodes génériques: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
{
}
Bien que la conversion de ces méthodes en universelles semble inutile à première vue, elle permet en fait d'éviter le packaging avec allocation dans le cas où il IntValue
s'agit d'une structure.Abonnements CancellationToken toujours en ligne
Lorsque vous annulez CancellationTokenSource
, tous les abonnements seront exécutés dans le thread actuel. Cela peut entraîner des pauses imprévues ou même des blocages implicites.var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel();
Vous ne pouvez pas échapper à ce comportement. Par conséquent, lors de l'annulation CancellationTokenSource
, demandez-vous si vous pouvez autoriser la capture de votre thread actuel en toute sécurité. Si la réponse est non, encapsulez l'appel à l' Cancel
intérieur Task.Run
pour l'exécuter dans le pool de threads.Les continuations de TaskCompletionSource sont souvent en ligne
Comme les abonnements CancellationToken
, les continuations TaskCompletionSource
sont souvent en ligne. Il s'agit d'une bonne optimisation, mais elle peut provoquer des erreurs implicites. Par exemple, considérez le programme suivant: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");
}
}
L'appel tcs.SetResult
provoque l' await ProcessAsync()
exécution de la continuation dans le thread actuel. Par conséquent, l'instruction _mutex.Wait()
est exécutée par le même thread qu'elle doit appeler _mutex.Set()
, ce qui conduit à un blocage. Cela peut être évité en passant le paramètre TaskCreationsOptions.RunContinuationsAsynchronously
c TaskCompletionSource
.Si vous n'avez aucune bonne raison de le négliger, utilisez toujours l'option TaskCreationsOptions.RunContinuationsAsynchronously
lors de la création TaskCompletionSource
.Attention: le code sera également compilé si vous l'utilisez à la TaskContinuationOptions.RunContinuationsAsynchronously
place TaskCreationOptions.RunContinuationsAsynchronously
, mais les paramètres seront ignorés et les suites seront toujours en ligne. Il s'agit d'une erreur étonnamment courante car elle TaskContinuationOptions
précède la TaskCreationOptions
saisie semi-automatique.Task.Run / Task.Factory.StartNew
Si vous n'avez aucune raison de l'utiliser Task.Factory.StartNew
, choisissez toujours Task.Run
d'exécuter une tâche en arrière-plan. Task.Run
utilise des valeurs par défaut plus sûres, et plus important encore, il décompresse automatiquement la tâche renvoyée, ce qui peut éviter les erreurs non évidentes avec les méthodes asynchrones. Considérez le programme suivant: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();
}
}
Malgré son apparence, la fin du programme sera affichée plus tôt que le traitement terminé. En effet, il Task.Factory.StartNew
reviendra Task<Task>
et le code n'attend qu'une tâche externe. Le code correct pourrait être soit await Task.Factory.StartNew(ProcessAsync).Unwrap()
, soit await Task.Run(ProcessAsync)
.Il n'y a que trois cas d'utilisation valides Task.Factory.StartNew
:- Exécution d'une tâche dans un autre planificateur.
- Exécution d'une tâche dans un thread dédié (à l'aide de
TaskCreationOptions.LongRunning
). - (
TaskCreationOptions.PreferFairness
).
.