Hallo alle zusammen. Wir haben am Vorabend des Kursbeginns "C # Developer" eine Übersetzung eines weiteren nützlichen Materials vorbereitet . Viel Spaß beim Lesen.
Da ich kürzlich eine Liste mit Best Practices in C # für Criteo erstellt habe, dachte ich, es wäre schön, diese öffentlich zu teilen. Der Zweck dieses Artikels besteht darin, eine unvollständige Liste von Codevorlagen bereitzustellen, die vermieden werden sollten, entweder weil sie fragwürdig sind oder weil sie nur schlecht funktionieren. Die Liste mag etwas zufällig erscheinen, da sie leicht aus dem Kontext gerissen ist, aber alle ihre Elemente wurden irgendwann in unserem Code gefunden und verursachten Probleme in der Produktion. Ich hoffe, dies wird als gute Prävention dienen und Ihre Fehler in Zukunft verhindern.Beachten Sie auch, dass Criteo-Webdienste auf Hochleistungscode basieren, sodass ineffizienter Code vermieden werden muss. In den meisten Anwendungen ist kein spürbarer Unterschied zum Ersetzen einiger dieser Vorlagen erkennbar.Und zu guter Letzt wurden einige Punkte (zum Beispiel ConfigureAwait
) bereits in vielen Artikeln erörtert, daher werde ich nicht näher darauf eingehen. Ziel ist es, eine kompakte Liste von Punkten zu erstellen, auf die Sie achten müssen, und keine detaillierte technische Beschreibung der einzelnen Punkte zu geben.Synchrones Warten auf asynchronen Code
Erwarten Sie niemals synchron unfertige Aufgaben. Dies gilt unter anderem für : Task.Wait, Task.Result, Task.GetAwaiter().GetResult(), Task.WaitAny, Task.WaitAll
.Als Verallgemeinerung: Jede synchrone Beziehung zwischen zwei Pool-Threads kann zu einer Erschöpfung des Pools führen. Die Ursachen dieses Phänomens werden in diesem Artikel beschrieben .ConfigureAwait
Wenn Ihr Code aus einem Synchronisationskontext aufgerufen werden kann, verwenden Sie ihn ConfigureAwait(false)
für jeden Ihrer erwarteten Anrufe.Bitte beachten Sie, dass dies ConfigureAwait
nur bei Verwendung eines Schlüsselworts nützlich ist await
.Der folgende Code ist beispielsweise bedeutungslos:
var result = ProcessAsync().ConfigureAwait(false).GetAwaiter().GetResult();
asynchrone Leere
Niemals benutzenasync void
. Die in der async void
Methode ausgelöste Ausnahme wird in den Synchronisationskontext übertragen und führt normalerweise zum Absturz der gesamten Anwendung.Wenn Sie die Aufgabe in Ihrer Methode nicht zurückgeben können (z. B. weil Sie die Schnittstelle implementieren), verschieben Sie den asynchronen Code in eine andere Methode und rufen Sie ihn auf:interface IInterface
{
void DoSomething();
}
class Implementation : IInterface
{
public void DoSomething()
{
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync()
{
await Task.Delay(100);
}
}
Vermeiden Sie nach Möglichkeit Asynchronität
Aus Gewohnheit oder wegen des Muskelgedächtnisses können Sie etwas schreiben wie:public async Task CallAsync()
{
var client = new Client();
return await client.GetAsync();
}
Obwohl der Code semantisch korrekt ist, ist die Verwendung eines Schlüsselworts async
hier nicht erforderlich und kann in einer stark belasteten Umgebung zu erheblichem Overhead führen. Versuchen Sie es nach Möglichkeit zu vermeiden:public Task CallAsync()
{
var client = new Client();
return _client.GetAsync();
}
Beachten Sie jedoch, dass Sie nicht auf diese Optimierung zurückgreifen können, wenn Ihr Code in Blöcke eingeschlossen ist (z. B. try/catch
oder 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();
}
}
In der falschen Version ( Incorrect()
) wird der Client möglicherweise gelöscht, bevor der GetAsync
Aufruf abgeschlossen ist , da die Aufgabe innerhalb des using-Blocks nicht durch Warten erwartet wird.Regionale Vergleiche
Wenn Sie keinen Grund haben, regionale Vergleiche zu verwenden, verwenden Sie immer ordinale Vergleiche . Obwohl dies aufgrund interner Optimierungen für die Datenpräsentationsformulare in den USA keine große Rolle spielt, ist der Vergleich für die Präsentationsformen anderer Regionen um eine Größenordnung langsamer (und unter Linux bis zu zwei Größenordnungen!). Da der String-Vergleich in den meisten Anwendungen häufig vorkommt, steigt der Overhead erheblich.ConcurrentBag <
T.>
Niemals ConcurrentBag<
T>
ohne Benchmarking verwenden . Diese Sammlung wurde für ganz bestimmte Anwendungsfälle entwickelt (wenn das Element die meiste Zeit von dem Thread, der es in die Warteschlange gestellt hat, aus der Warteschlange ausgeschlossen wird) und unter schwerwiegenden Leistungsproblemen leidet, wenn es für andere Zwecke verwendet wird. Wenn Sie eine Thread-sichere Sammlung benötigen, bevorzugen ConcurrentQueue <
T>
.ReaderWriterLock / ReaderWriterLockSlim <
T >
Niemals ohne Benchmarking verwenden. ReaderWriterLock<T>
/ReaderWriterLockSlim<T>
Obwohl die Verwendung dieser Art von spezialisiertem Synchronisationsprimitiv bei der Arbeit mit Lesern und Schreibern verlockend sein kann, sind die Kosten viel höher als bei einfachen Monitor
(mit einem Schlüsselwort verwendeten lock
). Wenn die Anzahl der Leser, die gleichzeitig den kritischen Abschnitt ausführen, nicht sehr groß ist, reicht die Parallelität nicht aus, um den erhöhten Overhead zu absorbieren, und der Code funktioniert schlechter.
Bevorzugen Sie Lambda-Funktionen anstelle von Methodengruppen
Betrachten Sie den folgenden Code:public IEnumerable<int> GetItems()
{
return _list.Where(i => Filter(i));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Resharper schlägt vor, Code ohne Lambda-Funktion neu zu schreiben, was möglicherweise etwas sauberer aussieht:public IEnumerable<int> GetItems()
{
return _list.Where(Filter);
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Leider führt dies zur Zuweisung von dynamischem Speicher für jeden Anruf. Tatsächlich wird der Aufruf wie folgt kompiliert:public IEnumerable<int> GetItems()
{
return _list.Where(new Predicate<int>(Filter));
}
private static bool Filter(int element)
{
return i % 2 == 0;
}
Dies kann erhebliche Auswirkungen auf die Leistung haben, wenn der Code in einem stark belasteten Abschnitt aufgerufen wird.Durch die Verwendung von Lambda-Funktionen wird die Compileroptimierung gestartet, bei der der Delegat in einem statischen Feld zwischengespeichert wird, wodurch eine Zuordnung vermieden wird. Dies funktioniert nur, wenn Filter
statisch. Wenn nicht, können Sie den Delegaten selbst zwischenspeichern: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;
}
Aufzählungen in Zeichenfolgen konvertieren
Der Aufruf Enum.ToString
in .net
ist recht teuer, weil Reflexion verwendet wird , nach innen zu konvertieren, und die virtuelle Methode auf die Struktur provoziert Aufruf Verpackung. Dies sollte so weit wie möglich vermieden werden.Aufzählungen können oft durch konstante Zeichenfolgen ersetzt werden:
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";
}
Wenn Sie wirklich eine Aufzählung verwenden müssen, sollten Sie den konvertierten Wert in einem Wörterbuch zwischenspeichern, um den Overhead zu amortisieren.Listenvergleich
Hinweis: Dies ist in .net Core nicht mehr relevant, da die Optimierung ab Version 2.1 automatisch von JIT durchgeführt wird.
Wenn Sie Aufzählungen als Flags verwenden, kann es verlockend sein, die folgende Methode zu verwenden Enum.HasFlag
:[Flags]
public enum Options
{
Option1 = 1,
Option2 = 2,
Option3 = 4
}
private Options _option;
public bool IsOption2Enabled()
{
return _option.HasFlag(Options.Option2);
}
Dieser Code provoziert zwei Pakete mit Zuordnung: eines für die Konvertierung Options.Option2
in Enum
und das andere für einen virtuellen Aufruf HasFlag
der Struktur. Dies macht diesen Code unverhältnismäßig teuer. Stattdessen sollten Sie die Lesbarkeit opfern und binäre Operatoren verwenden:public bool IsOption2Enabled()
{
return (_option & Options.Option2) == Options.Option2;
}
Implementierung von Vergleichsmethoden für Strukturen
Wenn Sie eine Struktur in Vergleichen verwenden (z. B. als Schlüssel für ein Wörterbuch), müssen Sie die Methoden überschreiben Equals/GetHashCode
. Die Standardimplementierung verwendet Reflection und ist sehr langsam. Die von Resharper generierte Implementierung ist normalerweise ziemlich gut.Weitere Informationen hierzu finden Sie hier: devblogs.microsoft.com/premier-developer/performance-implications-of-default-struct-equality-in-cVermeiden Sie ungeeignete Verpackungen, wenn Sie Strukturen mit Schnittstellen verwenden
Betrachten Sie den folgenden Code: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)
{
}
Machen IntValue
strukturiert kann verlockend sein , dynamische Zuweisung von Speicher zu vermeiden. Da AddValue
sie jedoch SendValue
eine Schnittstelle erwarten und die Schnittstellen eine Referenzsemantik aufweisen, wird der Wert bei jedem Aufruf gepackt, wodurch die Vorteile dieser "Optimierung" zunichte gemacht werden. Tatsächlich gibt es sogar noch mehr Speicherzuweisungen als bei IntValue
einer Klasse, da der Wert für jeden Aufruf unabhängig gepackt wird.Wenn Sie eine API schreiben und erwarten, dass einige Werte Strukturen sind, verwenden Sie generische Methoden: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
{
}
Obwohl die Konvertierung dieser Methoden in Universal auf den ersten Blick nutzlos erscheint, können Sie in dem Fall, in dem es sich IntValue
um eine Struktur handelt , das Verpacken mit Zuordnung vermeiden .CancellationToken-Abonnements immer inline
Wenn Sie kündigen CancellationTokenSource
, werden alle Abonnements im aktuellen Thread ausgeführt. Dies kann zu ungeplanten Pausen oder sogar zu impliziten Deadlocks führen.var cts = new CancellationTokenSource();
cts.Token.Register(() => Thread.Sleep(5000));
cts.Cancel();
Sie können sich diesem Verhalten nicht entziehen. CancellationTokenSource
Fragen Sie sich daher beim Abbrechen , ob Sie die Erfassung Ihres aktuellen Threads sicher zulassen können. Wenn die Antwort nein ist, wickeln Sie den Anruf Cancel
innerhalb Task.Run
es in dem Thread - Pool auszuführen.TaskCompletionSource-Fortsetzungen werden häufig inline ausgeführt
Wie bei Abonnements CancellationToken
sind Fortsetzungen TaskCompletionSource
häufig inline. Dies ist eine gute Optimierung, kann jedoch implizite Fehler verursachen. Betrachten Sie beispielsweise das folgende Programm: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");
}
}
Der Aufruf tcs.SetResult
bewirkt , dass die Fortsetzung await ProcessAsync()
im aktuellen Thread ausgeführt wird. Daher wird die Anweisung _mutex.Wait()
von demselben Thread ausgeführt, den sie aufrufen soll _mutex.Set()
, was zu einem Deadlock führt. Dies kann durch Übergabe des Parameters TaskCreationsOptions.RunContinuationsAsynchronously
c vermieden werden TaskCompletionSource
.Wenn Sie keinen guten Grund haben, dies zu vernachlässigen, verwenden Sie TaskCreationsOptions.RunContinuationsAsynchronously
beim Erstellen immer die Option TaskCompletionSource
.Seien Sie vorsichtig: Der Code wird auch kompiliert, wenn Sie TaskContinuationOptions.RunContinuationsAsynchronously
stattdessen verwenden TaskCreationOptions.RunContinuationsAsynchronously
, aber die Parameter werden ignoriert und die Fortsetzungen bleiben weiterhin inline. Dies ist ein überraschend häufiger Fehler, da er TaskContinuationOptions
der TaskCreationOptions
automatischen Vervollständigung vorausgeht .Task.Run / Task.Factory.StartNew
Wenn Sie keinen Grund zur Verwendung haben Task.Factory.StartNew
, wählen Sie immer Task.Run
eine Hintergrundaufgabe aus. Task.Run
Verwendet sicherere Standardwerte und entpackt vor allem automatisch die zurückgegebene Aufgabe, wodurch nicht offensichtliche Fehler mit asynchronen Methoden verhindert werden können. Betrachten Sie das folgende Programm: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();
}
}
Trotz seines Erscheinungsbilds wird das Programmende früher als die Verarbeitung angezeigt. Dies liegt daran, dass es zurückgegeben Task.Factory.StartNew
wird Task<Task>
und der Code nur eine externe Aufgabe erwartet. Der richtige Code könnte entweder await Task.Factory.StartNew(ProcessAsync).Unwrap()
oder sein await Task.Run(ProcessAsync)
.Es gibt nur drei gültige Anwendungsfälle Task.Factory.StartNew
:- Ausführen einer Aufgabe in einem anderen Scheduler.
- Ausführen einer Aufgabe in einem dedizierten Thread (mit
TaskCreationOptions.LongRunning
). - (
TaskCreationOptions.PreferFairness
).
.