Das Buch "Hochleistungscode auf der .NET-Plattform. 2. Auflage

Bild In diesem Buch erfahren Sie, wie Sie die Leistung von verwaltetem Code maximieren können, idealerweise ohne die Vorteile der .NET-Umgebung zu beeinträchtigen oder im schlimmsten Fall eine minimale Anzahl zu opfern. Sie lernen rationale Programmiermethoden kennen, finden heraus, was zu vermeiden ist und höchstwahrscheinlich, wie Sie die frei verfügbaren Tools verwenden, um das Produktivitätsniveau einfach zu messen. Das Schulungsmaterial enthält nur ein Minimum an Wasser - nur das Notwendigste. Das Buch gibt genau das, was Sie wissen müssen, es ist relevant und prägnant, enthält nicht zu viel. Die meisten Kapitel beginnen mit allgemeinen Informationen und Hintergrundinformationen, gefolgt von spezifischen Tipps, die wie ein Rezept aufgebaut sind, und enden mit einem schrittweisen Abschnitt zum Messen und Debuggen für eine Vielzahl von Szenarien.

Auf dem Weg dorthin wird Ben Watson in bestimmte Komponenten der .NET-Umgebung eintauchen, insbesondere in die darauf basierende Common Language Runtime (CLR). Wir werden sehen, wie der Arbeitsspeicher Ihres Computers verwaltet, Code generiert, die Multithread-Ausführung organisiert und vieles mehr getan wird . Sie werden gezeigt, wie die .NET-Architektur gleichzeitig Ihr Software-Tool einschränkt und zusätzliche Funktionen bietet und wie sich die Auswahl der Programmierpfade erheblich auf die Gesamtleistung der Anwendung auswirken kann. Als Bonus wird der Autor Ihnen Geschichten aus der Erfahrung mit der Erstellung sehr großer, komplexer und leistungsstarker .NET-Systeme bei Microsoft in den letzten neun Jahren mitteilen.

Auszug: Wählen Sie die entsprechende Thread-Pool-Größe


Im Laufe der Zeit wird der Thread-Pool unabhängig konfiguriert, hat jedoch zu Beginn keinen Verlauf und startet im Ausgangszustand. Wenn Ihr Softwareprodukt außergewöhnlich asynchron ist und einen Zentralprozessor erheblich verwendet, kann es unter unerschwinglich hohen Kosten für den Erststart leiden, bis noch mehr Threads erstellt und verfügbar sind. Passen Sie die Startparameter so an, dass Sie ab dem Start der Anwendung eine bestimmte Anzahl vorgefertigter Threads zur Hand haben:

const int MinWorkerThreads = 25; const int MinIoThreads = 25; ThreadPool.SetMinThreads(MinWorkerThreads, MinIoThreads); 

Sei hier vorsichtig. Bei Verwendung von Task-Objekten basiert deren Versand auf der Anzahl der dafür verfügbaren Threads. Wenn zu viele von ihnen vorhanden sind, können Task-Objekte einer übermäßigen Planung unterzogen werden, was zumindest zu einer Verringerung der Effizienz des Zentralprozessors aufgrund einer häufigeren Kontextumschaltung führt. Wenn die Arbeitslast nicht so hoch ist, kann der Thread-Pool auf einen Algorithmus umschalten, der die Anzahl der Threads reduziert und auf eine niedrigere als die angegebene Anzahl bringt.

Sie können ihre maximale Anzahl auch mit der SetMaxThreads-Methode festlegen. Diese Technik unterliegt jedoch ähnlichen Risiken.

Um die erforderliche Anzahl von Threads herauszufinden, lassen Sie diesen Parameter in Ruhe und analysieren Sie Ihre Anwendung in einem stabilen Zustand mit den Methoden ThreadPool.GetMaxThreads und ThreadPool.GetMinThreads oder Leistungsindikatoren, die die Anzahl der am Prozess beteiligten Threads anzeigen.

Flüsse nicht unterbrechen


Das Unterbrechen der Arbeit von Threads ohne Abstimmung mit der Arbeit anderer Threads ist ein ziemlich gefährlicher Vorgang. Streams müssen sich selbst bereinigen, und wenn sie als Abort-Methode bezeichnet werden, können sie nicht ohne negative Konsequenzen geschlossen werden. Wenn ein Thread zerstört wird, befinden sich Teile der Anwendung in einem undefinierten Zustand. Es wäre besser, das Programm zum Absturz zu bringen, aber im Idealfall ist ein sauberer Neustart erforderlich.

Um einen Thread sicher zu beenden, müssen Sie einen freigegebenen Status verwenden, und die Thread-Funktion selbst muss diesen Status überprüfen, um zu bestimmen, wann er abgeschlossen werden soll. Sicherheit muss durch Kohärenz erreicht werden.

Im Allgemeinen sollten Sie immer Aufgabenobjekte verwenden - es wird keine API zum Unterbrechen einer Aufgabe bereitgestellt. Um einen Thread konsistent beenden zu können, müssen Sie, wie bereits erwähnt, das CancellationToken-Token verwenden.

Ändern Sie die Thread-Priorität nicht


Im Allgemeinen ist das Ändern der Priorität von Threads ein äußerst erfolgloses Unterfangen. Unter Windows wird der Thread-Versand entsprechend ihrer Prioritätsstufe durchgeführt. Wenn Threads mit hoher Priorität immer zur Ausführung bereit sind, werden Threads mit niedriger Priorität ignoriert und erhalten selten eine Chance zum Ausführen. Indem Sie die Priorität eines Threads erhöhen, sagen Sie, dass seine Arbeit Vorrang vor allen anderen Arbeiten haben sollte, einschließlich anderer Prozesse. Dies ist für ein stabiles System nicht sicher.

Es ist besser, die Priorität des Threads zu verringern, wenn etwas ausgeführt wird, das bis zum Abschluss von Aufgaben mit normaler Priorität warten kann. Ein guter Grund, die Priorität eines Threads zu verringern, kann darin bestehen, einen außer Kontrolle geratenen Thread zu entdecken, der eine Endlosschleife ausführt. Es ist unmöglich, einen Thread sicher zu unterbrechen. Die einzige Möglichkeit, einen bestimmten Thread und Prozessorressourcen zurückzugeben, besteht darin, den Prozess neu zu starten. Bis es möglich wird, den Stream zu schließen und sauber zu machen, ist eine Verringerung der Priorität des außer Kontrolle geratenen Streams ein vernünftiger Weg, um die Folgen zu minimieren. Es sollte beachtet werden, dass selbst Threads mit einer niedrigeren Priorität im Laufe der Zeit garantiert ausgeführt werden: Je länger ihnen die Starts entzogen werden, desto höher wird ihre dynamische Priorität von Windows festgelegt. Eine Ausnahme bildet die Leerlaufpriorität THREAD_ - PRIORITY_IDLE, bei der das Betriebssystem die Ausführung eines Threads nur dann plant, wenn buchstäblich nichts mehr zu starten ist.

Es kann durchaus berechtigte Gründe geben, die Priorität des Flusses zu erhöhen, beispielsweise die Notwendigkeit, schnell auf seltene Situationen zu reagieren. Die Verwendung solcher Techniken sollte jedoch sehr vorsichtig sein. Das Planen von Threads in Windows wird unabhängig von den Prozessen ausgeführt, zu denen sie gehören. Daher wird ein Thread mit hoher Priorität aus Ihrem Prozess auf Kosten nicht nur Ihrer anderen Threads, sondern auch aller Threads anderer Anwendungen, die auf Ihrem System ausgeführt werden, gestartet.

Wenn ein Thread-Pool verwendet wird, werden alle Prioritätsänderungen jedes Mal verworfen, wenn ein Thread in den Pool zurückkehrt. Wenn Sie bei Verwendung der Task Parallel-Bibliothek weiterhin grundlegende Threads verwalten, sollten Sie berücksichtigen, dass mehrere Tasks im selben Thread gestartet werden können, bevor sie an den Pool zurückgegeben werden.

Thread-Synchronisation und Blockierung


Sobald die Konversation zu mehreren Threads kommt, müssen diese synchronisiert werden. Die Synchronisierung besteht darin, nur einem Thread Zugriff auf einen gemeinsam genutzten Status zu gewähren, z. B. auf ein Klassenfeld. Normalerweise werden Threads mithilfe von Synchronisationsobjekten wie Monitor, Semaphore, ManualResetEvent usw. synchronisiert. Manchmal werden sie informell als Sperren bezeichnet, und der Synchronisationsprozess in einem bestimmten Thread wird als Sperre bezeichnet.

Eine der grundlegenden Wahrheiten von Sperren ist folgende: Sie steigern niemals die Leistung. Im besten Fall - mit einem gut implementierten Synchronisationsprimitiv und ohne Konkurrenz - kann das Blockieren neutral sein. Dies führt dazu, dass die Ausführung nützlicher Arbeit durch andere Threads gestoppt wird und die CPU-Zeit verschwendet wird, die Kontextwechselzeit erhöht wird und andere negative Konsequenzen entstehen. Sie müssen sich damit abfinden, denn Korrektheit ist viel wichtiger als einfache Leistung. Ob das falsche Ergebnis schnell berechnet wird, spielt keine Rolle!

Bevor Sie beginnen, das Problem der Verwendung des Schließgeräts zu lösen, werden wir die grundlegendsten Prinzipien betrachten.

Muss ich mich überhaupt um die Leistung kümmern?


Begründen Sie zunächst die Notwendigkeit, die Produktivität zu steigern. Dies bringt uns zurück zu den in Kapitel 1 beschriebenen Prinzipien. Die Leistung ist nicht für Ihren gesamten Anwendungscode gleich wichtig. Nicht jeder Code muss einer Optimierung n-ten Grades unterzogen werden. In der Regel beginnt alles mit der „inneren Schleife“ - dem Code, der am häufigsten oder am kritischsten für die Leistung ausgeführt wird - und erstreckt sich in alle Richtungen, bis die Kosten den erhaltenen Nutzen übersteigen. Es gibt viele Bereiche im Code, die für die Leistung viel weniger wichtig sind. Wenn Sie in einer solchen Situation ein Schloss benötigen, wenden Sie es ruhig an.

Und jetzt solltest du vorsichtig sein. Wenn Ihr unkritischer Code in einem Thread aus einem Thread-Pool ausgeführt wird und Sie ihn für eine lange Zeit blockieren, fügt der Thread-Pool möglicherweise weitere Threads ein, um andere Anforderungen zu verarbeiten. Wenn ein oder zwei Threads dies von Zeit zu Zeit tun, ist das in Ordnung. Wenn jedoch viele Threads solche Dinge tun, kann ein Problem auftreten, da aus diesem Grund Ressourcen, die die eigentliche Arbeit erledigen müssen, nutzlos ausgegeben werden. Unbeabsichtigtes Starten eines Programms mit einer erheblichen konstanten Last kann sich negativ auf das System auswirken, selbst wenn Teile mit hoher Leistung aufgrund unnötiger Kontextwechsel oder unangemessener Beteiligung des Thread-Pools unwichtig sind. Wie in allen anderen Fällen müssen Messungen durchgeführt werden, um die Situation zu beurteilen.

Benötigen Sie wirklich ein Schloss?


Der effektivste Verriegelungsmechanismus ist einer, der nicht ist. Wenn Sie die Notwendigkeit einer Thread-Synchronisierung vollständig beseitigen können, ist dies der beste Weg, um eine hohe Leistung zu erzielen. Dies ist ein Ideal, das nicht so einfach zu erreichen ist. In der Regel bedeutet dies, dass Sie sicherstellen müssen, dass kein veränderbarer gemeinsamer Status vorhanden ist. Jede Anforderung, die Ihre Anwendung durchläuft, kann unabhängig von einer anderen Anforderung oder einigen zentralisierten veränderlichen (Lese- / Schreib-) Daten verarbeitet werden. Diese Funktion ist das beste Szenario, um eine hohe Leistung zu erzielen.

Und sei trotzdem vorsichtig. Mit der Umstrukturierung ist es einfach, über Bord zu gehen und Code in ein chaotisches Chaos zu verwandeln, das niemand, einschließlich Sie selbst, herausfinden kann. Sie sollten nicht zu weit gehen, es sei denn, eine hohe Produktivität ist wirklich ein kritischer Faktor und kann sonst nicht erreicht werden. Verwandeln Sie den Code in asynchron und unabhängig, damit er klar bleibt.

Wenn mehrere Threads nur aus einer Variablen gelesen werden (und es keine Hinweise zum Schreiben aus einem Stream gibt), ist keine Synchronisierung erforderlich. Alle Threads können uneingeschränkt zugreifen. Dies gilt automatisch für unveränderliche Objekte wie Zeichenfolgen oder Werte unveränderlicher Typen, kann jedoch für alle Objekttypen gelten, wenn Sie die Unveränderlichkeit ihres Werts beim Lesen durch mehrere Threads garantieren.

Wenn mehrere Threads in eine gemeinsam genutzte Variable schreiben, prüfen Sie, ob der synchronisierte Zugriff durch die Verwendung einer lokalen Variablen beseitigt werden kann. Wenn Sie eine temporäre Kopie für die Arbeit erstellen können, entfällt die Notwendigkeit einer Synchronisierung. Dies ist besonders wichtig für wiederholten synchronisierten Zugriff. Nach dem erneuten Zugriff auf die gemeinsam genutzte Variable müssen Sie nach dem einmaligen Zugriff auf die gemeinsam genutzte Variable zum erneuten Zugriff auf die lokale Variable übergehen, wie im folgenden einfachen Beispiel zum Hinzufügen von Elementen zu einer von mehreren Threads gemeinsam genutzten Sammlung.

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { for (int j = 0; j < 5000000; j++) { lock (syncObj) { masterList.Add(j); } } }); } Task.WaitAll(tasks); 

Dieser Code kann wie folgt konvertiert werden:

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { var localList = new List<long >(); for (int j = 0; j < 5000000; j++) { localList.Add(j); } lock (syncObj) { masterList.AddRange(localList); } }); } Task.WaitAll(tasks); 

Auf meinem Computer läuft die zweite Version des Codes mehr als doppelt so schnell wie die erste.
Letztendlich ist ein veränderlicher gemeinsamer Staat ein grundlegender Feind der Leistung. Für die Datensicherheit ist eine Synchronisierung erforderlich, die die Leistung beeinträchtigt. Wenn Ihr Design zumindest die geringste Möglichkeit hat, Blockierungen zu vermeiden, stehen Sie kurz vor der Implementierung eines idealen Multithread-Systems.

Präferenzreihenfolge synchronisieren


Bei der Entscheidung, ob irgendeine Art von Synchronisation erforderlich ist, sollte berücksichtigt werden, dass nicht alle die gleichen Leistungs- oder Verhaltensmerkmale aufweisen. In den meisten Situationen müssen Sie nur ein Schloss verwenden, und normalerweise sollte dies die ursprüngliche Option sein. Die Verwendung von etwas anderem als Blockieren erfordert intensive Messungen, um zusätzliche Komplexität zu rechtfertigen. Im Allgemeinen betrachten wir die Synchronisationsmechanismen in der folgenden Reihenfolge.

1. Lock / Class Monitor - sorgt für Einfachheit, Verständlichkeit des Codes und ein ausgewogenes Leistungsverhältnis.

2. Das völlige Fehlen der Synchronisation. Gemeinsame veränderbare Zustände beseitigen, umstrukturieren und optimieren. Dies ist schwieriger, aber wenn es erfolgreich ist, funktioniert es im Grunde besser als das Anwenden von Blockierung (außer wenn Fehler gemacht werden oder die Architektur beeinträchtigt wird).

3. Einfache Verriegelungsmethoden - In einigen Szenarien ist dies möglicherweise besser geeignet. Sobald die Situation jedoch komplizierter wird, verwenden Sie die Verriegelungssperre.

Und schließlich, wenn Sie die Vorteile ihrer Verwendung wirklich nachweisen können, verwenden Sie komplexere, komplexere Schlösser (denken Sie daran: Sie erweisen sich selten als so nützlich, wie Sie es erwarten):

  1. asynchrone Sperren (wird später in diesem Kapitel erläutert);
  2. alle anderen.

Bestimmte Umstände können die Verwendung einiger dieser Technologien diktieren oder behindern. Beispielsweise ist es unwahrscheinlich, dass das Kombinieren mehrerer Interlocked-Methoden eine einzelne Lock-Anweisung übertrifft.

»Weitere Informationen zum Buch finden Sie auf der Website des Herausgebers
» Inhalt
» Auszug

25% Rabatt auf Gutschein für Händler - .NET

Nach Bezahlung der Papierversion des Buches wird ein elektronisches Buch per E-Mail verschickt.

Source: https://habr.com/ru/post/de458520/


All Articles