Effektive Nutzung von libdispatch

( Hinweis: Der Autor des Originalmaterials ist Thomas @tclementdev, ein Benutzer von Github und Twitter . Die vom Autor verwendete Ich -Erzählung wird in der folgenden Übersetzung gespeichert. )

Ich denke, dass die meisten Entwickler libdispatch ineffizient verwenden, weil es in der Community eingeführt wurde und weil Dokumentation und APIs verwirrend sind. Ich kam zu diesem Gedanken, nachdem ich die Diskussion über „Parallelität“ in der Swift-Entwicklungs-Mailingliste (Swift-Evolution) gelesen hatte. Besonders aufgeklärt sind die Botschaften von Pierre Habouzit (Pierre Habouzit - engagiert sich für libdispatch in Apple):


Er hat auch viele Tweets zu diesem Thema:


Von mir gemacht:

  • Das Programm sollte nur sehr wenige Warteschlangen haben, die den globalen Pool verwenden ( Threads - ca. Per. ). Wenn alle diese Warteschlangen gleichzeitig aktiv sind, erhalten Sie die gleiche Anzahl gleichzeitig laufender Threads. Diese Warteschlangen sollten als Ausführungskontexte im Programm betrachtet werden (GUI, Speicher, Arbeit im Hintergrund, ...), die von der gleichzeitigen Ausführung profitieren.
  • Beginnen Sie mit der sequentiellen Ausführung. Wenn Sie ein Leistungsproblem feststellen, nehmen Sie Messungen vor, um die Ursache herauszufinden. Und wenn die parallele Ausführung hilft, gehen Sie vorsichtig damit um. Stellen Sie immer sicher, dass der Parallelcode unter dem Druck des Systems funktioniert. Verwenden Sie Warteschlangen standardmäßig wieder. Fügen Sie Linien hinzu, wenn dies messbare Vorteile bringt. Die meisten Anwendungen sollten nicht mehr als drei bis vier Warteschlangen verwenden.
  • Warteschlangen, für die eine andere Warteschlange als Ziel festgelegt wurde, funktionieren gut und lassen sich skalieren.
    ( Hinweis perev .: Über das Festlegen einer Warteschlange als Ziel für eine andere Warteschlange kann beispielsweise hier gelesen werden . )
  • Verwenden Sie nicht dispatch_get_global_queue (). Dies ist nicht mit der Servicequalität und den Prioritäten vereinbar und kann zu einem explosionsartigen Anstieg der Anzahl der Flüsse führen. Führen Sie Ihren Code stattdessen in einem Ihrer Ausführungskontexte aus.
  • dispatch_async () ist eine Verschwendung von Ressourcen für kleine ausführbare Blöcke (<1 ms), da dieser Aufruf aufgrund des übermäßigen Eifers von libdispatch höchstwahrscheinlich die Erstellung eines neuen Threads erfordert. Anstatt den Ausführungskontext zu wechseln, um den gemeinsam genutzten Status zu schützen, verwenden Sie Sperrmechanismen, um gleichzeitig auf den gemeinsam genutzten Status zuzugreifen.
  • Einige Klassen / Bibliotheken sind insofern gut konzipiert, als sie den Ausführungskontext wiederverwenden, den der aufrufende Code an sie übergibt. Dies ermöglicht die Verwendung einer herkömmlichen Verriegelung, um die Gewindesicherheit zu gewährleisten. os_unfair_lock ist normalerweise der schnellste Sperrmechanismus im System: Es funktioniert besser mit Prioritäten und verursacht weniger Kontextwechsel.
  • Bei paralleler Ausführung sollten Ihre Aufgaben nicht untereinander zu kämpfen haben, da sonst die Produktivität stark sinkt. Kämpfe nehmen viele Formen an. Der offensichtliche Fall: der Kampf um die Eroberung des Schlosses. In Wirklichkeit bedeutet ein solcher Kampf jedoch nichts anderes als die Verwendung einer gemeinsam genutzten Ressource, die zu einem Engpass wird: IPC (Interprozesskommunikation) / OS-Dämonen, Malloc (Blockierung), gemeinsamer Speicher, E / A.
  • Sie benötigen nicht den gesamten Code, um asynchron ausgeführt zu werden, um eine explosive Erhöhung der Anzahl der Threads zu vermeiden. Es ist viel besser, eine begrenzte Anzahl niedrigerer Warteschlangen zu verwenden und die Verwendung von dispatch_get_global_queue () abzulehnen.
    ( Anmerkung 1: Anscheinend ist dies ein Fall, wenn die Anzahl der Threads beim Synchronisieren einer großen Anzahl paralleler Aufgaben explosionsartig zunimmt. „Wenn ich viele Blöcke habe und alle warten möchten, können wir das bekommen, was wir Thread nennen Explosion. " )
    ( Anmerkung S. 2: Aus der Diskussion geht hervor, dass Pierre Habuzit die Warteschlangen "die dem Kernel bekannt sind, wenn sie Aufgaben haben" unter den unteren Warteschlangen bedeutet. Hier sprechen wir über den Kernel des Betriebssystems. )
  • Wir dürfen nicht die Komplexität und die Fehler vergessen, die in einer Architektur auftreten, die mit asynchroner Ausführung und Rückrufen gefüllt ist. Sequentiell ausführbarer Code ist immer noch viel einfacher zu lesen, zu schreiben und zu warten.
  • Wettbewerbswarteschlangen sind weniger optimiert als sequentielle. Verwenden Sie sie, wenn Sie Leistungssteigerungen messen, andernfalls handelt es sich um eine vorzeitige Optimierung.
  • Wenn Sie Aufgaben in einer Warteschlange sowohl asynchron als auch synchron senden müssen, verwenden Sie anstelle von dispatch_sync () dispatch_async_and_wait (). dispatch_async_and_wait () garantiert nicht die Ausführung auf dem Thread, von dem der Aufruf stammt, wodurch die Kontextumschaltung reduziert wird, wenn die Zielwarteschlange aktiv ist.
    ( Hinweis Übersetzung 1: tatsächlich dispatch_sync () garantiert auch nicht, die Dokumentation darüber besagt nur "führt einen Block im aktuellen Thread aus, wann immer dies möglich ist. Mit einer Ausnahme: Der an die Hauptwarteschlange gesendete Block wird immer im Hauptthread ausgeführt. " )
    ( Hinweis Übersetzung 2: Über dispatch_async_and_wait () in der Dokumentation und im Quellcode )
  • Die korrekte Verwendung von 3-4 Kernen ist nicht so einfach. Die meisten, die es versuchen, können mit Skalierung und Energieverschwendung nicht umgehen, um die Produktivität geringfügig zu steigern. Wie Prozessoren mit Überhitzung arbeiten, hilft nicht weiter. Beispielsweise deaktiviert Intel Turbo-Boost, wenn genügend Kerne verwendet werden.
  • Messen Sie die Leistung Ihres Produkts in der realen Welt, um sicherzustellen, dass Sie es schneller und nicht langsamer machen. Seien Sie vorsichtig mit Mikroleistungstests - sie verbergen den Einfluss des Caches und halten den Thread-Pool heiß. Um zu überprüfen, was Sie tun, sollten Sie immer einen Makrotest durchführen.
  • libdispatch ist effektiv, aber es gibt keine Wunder. Die Ressourcen sind nicht endlos. Sie können die Realität des Betriebssystems und der Hardware, auf der der Code ausgeführt wird, nicht ignorieren. Außerdem ist nicht jeder Code gut parallelisiert.

Schauen Sie sich alle dispatch_async () -Aufrufe in Ihrem Code an und fragen Sie sich: Ist die Aufgabe, die Sie mit diesem Aufruf senden, den Kontextwechsel wirklich wert? In den meisten Fällen ist das Verriegeln wahrscheinlich die beste Wahl.

Sobald Sie Warteschlangen (Ausführungskontexte) aus einem vorgefertigten Satz verwenden und wiederverwenden, besteht die Gefahr von Deadlocks. Gefahr besteht beim Senden von Aufgaben an diese Warteschlangen mit dispatch_sync (). Dies geschieht normalerweise, wenn Warteschlangen zur Thread-Sicherheit verwendet werden. Also noch einmal: Die Lösung besteht darin, Sperrmechanismen zu verwenden und dispatch_async () nur dann zu verwenden, wenn Sie zu einem anderen Ausführungskontext wechseln müssen.

Ich persönlich habe durch die Befolgung dieser Richtlinien enorme Leistungsverbesserungen festgestellt.
(in hoch geladenen Programmen). Dies ist ein neuer Ansatz, der sich aber lohnt.

Weitere Links


Das Programm sollte nur sehr wenige Warteschlangen haben, die den globalen Pool verwenden


( Anmerkung perev .: Lesen des letzten Links, konnte nicht widerstehen und übertrug ein Stück aus der Mitte der Korrespondenz von Pierre Habuzit mit Chris Luttner. Unten ist eine der Antworten von Pierre Habuzit in 039420.html )
<...>
Ich verstehe, dass es für mich schwierig ist, meinen Standpunkt zu vermitteln, weil ich kein Mann in der Spracharchitektur bin, sondern ein Mann in der Systemarchitektur. Und ich verstehe Schauspieler definitiv nicht genug, um zu entscheiden, wie sie in das Betriebssystem integriert werden sollen. Wenn ich jedoch zum Datenbankbeispiel zurückkehre, unterscheiden sich die Actor-Database-Daten oder die Actor-Network-Interface aus der früheren Korrespondenz beispielsweise von dieser SQL-Abfrage oder dieser Netzwerkabfrage. Die ersten sind die Entitäten, die das Betriebssystem im Kernel kennen sollte. Während eine SQL-Abfrage oder eine Netzwerkabfrage nur Akteure sind, die in erster Linie zur Ausführung in die Warteschlange gestellt werden. Mit anderen Worten, diese Top-Level-Akteure unterscheiden sich, weil sie sich auf Top-Level befinden, direkt über der Kernel- / Low-Level-Laufzeit. Und das ist die Essenz, über die der Kern nachdenken sollte. Das macht sie großartig.

In der Versandbibliothek gibt es zwei Arten von Warteschlangen und die entsprechende API-Ebene:
  • globale Warteschlangen, die keine Warteschlangen wie die anderen sind. In Wirklichkeit sind sie nur eine Abstraktion über den Thread-Pool.
  • Alle anderen Warteschlangen, die Sie nach Belieben als Ziel für das andere festlegen können.

Heute ist klar geworden, dass dies ein Fehler war und dass es drei Arten von Warteschlangen geben sollte:

  • Globale Warteschlangen, bei denen es sich nicht um echte Warteschlangen handelt, die jedoch darstellen, welche Systemattributfamilie Ihr Ausführungskontext erfordert (hauptsächlich Prioritäten). Und wir müssen verbieten, Aufgaben direkt an diese Warteschlangen zu senden.
  • untere Warteschlangen (die GCD in den letzten Jahren verfolgt hat und im Quellcode "Basen" nennt ( es scheint, dass sich der Quellcode von GCD selbst auf - ungefähr übersetzt bezieht ). Die unteren Warteschlangen sind dem Kernel bekannt, wenn sie Aufgaben haben.
  • Alle anderen "internen" Warteschlangen, die der Kernel überhaupt nicht kennt.

In der Versandentwicklungsgruppe bedauern wir jeden Tag, dass der Unterschied zwischen der zweiten und dritten Gruppe von Warteschlangen in der API zunächst nicht deutlich gemacht wurde.

Ich nenne die zweite Gruppe gerne "Ausführungskontexte", aber ich kann verstehen, warum Sie sie Schauspieler nennen möchten. Dies ist vielleicht konsistenter (und die GCD hat dasselbe getan und dies und das als Warteschlange dargestellt). Solche "Akteure" der obersten Ebene sollten nur wenige sein, denn wenn sie alle gleichzeitig aktiv werden, benötigen sie dabei die gleiche Anzahl von Threads. Und dies ist keine Ressource, die skaliert werden kann. Deshalb ist es wichtig, zwischen ihnen zu unterscheiden. Und wie wir diskutieren, werden sie auch häufig verwendet, um einen gemeinsam genutzten Zustand, eine gemeinsame Ressource oder dergleichen zu schützen. Dies ist möglicherweise nicht mit internen Akteuren möglich.
<...>


Beginnen Sie mit der sequentiellen Ausführung.


Verwenden Sie keine globalen Warteschlangen


Hüten Sie sich vor Wettbewerbslinien


Verwenden Sie keine asynchronen Aufrufe, um den freigegebenen Status zu schützen


Verwenden Sie keine asynchronen Aufrufe für kleine Aufgaben


Einige Klassen / Bibliotheken sollten nur synchron sein


Der Kampf paralleler Aufgaben untereinander ist ein Killer der Produktivität


Verwenden Sie Sperrmechanismen, um Deadlocks zu vermeiden, wenn Sie einen gemeinsam genutzten Status schützen müssen


Verwenden Sie keine Semaphoren, um auf eine asynchrone Aufgabe zu warten


Die NSOperation-API weist einige schwerwiegende Fallstricke auf, die zu Leistungseinbußen führen können.


Vermeiden Sie Micro Performance Tests


Die Ressourcen sind nicht unbegrenzt


Über dispatch_async_and_wait ()


Die Verwendung von 3-4 Kernen ist nicht einfach


Viele Performance-Verbesserungen in iOS 12 wurden mit Single-Threaded-Daemons erzielt

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


All Articles