Es hängt lose mit dieser Frage zusammen: Sind std :: thread in C ++ 11 zusammengefasst? . Obwohl die Frage unterschiedlich ist, ist die Absicht dieselbe:
Frage 1: Ist es immer noch sinnvoll, eigene Thread-Pools (oder Bibliotheken von Drittanbietern) zu verwenden, um eine teure Thread-Erstellung zu vermeiden?
Die Schlussfolgerung in der anderen Frage war, dass Sie sich nicht darauf verlassen können, zusammengefasst std::thread
zu werden (es könnte sein oder es könnte nicht sein). Allerdings std::async(launch::async)
scheint eine viel höhere Chance zu haben , gebündelt werden.
Es glaubt nicht, dass es vom Standard erzwungen wird, aber meiner Meinung nach würde ich erwarten, dass alle guten C ++ 11-Implementierungen Thread-Pooling verwenden, wenn die Thread-Erstellung langsam ist. Nur auf Plattformen, auf denen es kostengünstig ist, einen neuen Thread zu erstellen, würde ich erwarten, dass sie immer einen neuen Thread erzeugen.
Frage 2: Dies ist genau das, was ich denke, aber ich habe keine Fakten, um dies zu beweisen. Ich kann mich sehr gut irren. Ist es eine fundierte Vermutung?
Schließlich habe ich hier einen Beispielcode bereitgestellt, der zunächst zeigt, wie die Thread-Erstellung meiner Meinung nach ausgedrückt werden kann durch async(launch::async)
:
Beispiel 1:
thread t([]{ f(); });
// ...
t.join();
wird
auto future = async(launch::async, []{ f(); });
// ...
future.wait();
Beispiel 2: Faden feuern und vergessen
thread([]{ f(); }).detach();
wird
// a bit clumsy...
auto dummy = async(launch::async, []{ f(); });
// ... but I hope soon it can be simplified to
async(launch::async, []{ f(); });
Frage 3: Würden Sie die async
Versionen den thread
Versionen vorziehen ?
Der Rest ist nicht mehr Teil der Frage, sondern nur zur Klarstellung:
Warum muss der Rückgabewert einer Dummy-Variablen zugewiesen werden?
Leider erzwingt der aktuelle C ++ 11-Standard, dass Sie den Rückgabewert von erfassen std::async
, da andernfalls der Destruktor ausgeführt wird, der blockiert, bis die Aktion beendet wird. Es wird von einigen als Fehler in der Norm angesehen (z. B. von Herb Sutter).
Dieses Beispiel von cppreference.com veranschaulicht es gut:
{
std::async(std::launch::async, []{ f(); });
std::async(std::launch::async, []{ g(); }); // does not run until f() completes
}
Eine weitere Klarstellung:
Ich weiß, dass Thread-Pools andere legitime Verwendungszwecke haben können, aber in dieser Frage interessiert mich nur der Aspekt, teure Kosten für die Thread-Erstellung zu vermeiden .
Ich denke, es gibt immer noch Situationen, in denen Thread-Pools sehr nützlich sind, insbesondere wenn Sie mehr Kontrolle über Ressourcen benötigen. Beispielsweise kann ein Server entscheiden, nur eine feste Anzahl von Anforderungen gleichzeitig zu verarbeiten, um schnelle Antwortzeiten zu gewährleisten und die Vorhersagbarkeit der Speichernutzung zu erhöhen. Thread-Pools sollten hier in Ordnung sein.
Thread-lokale Variablen können auch ein Argument für Ihre eigenen Thread-Pools sein, aber ich bin mir nicht sicher, ob sie in der Praxis relevant sind:
- Erstellen eines neuen Threads mit
std::thread
Starts ohne initialisierte threadlokale Variablen. Vielleicht ist das nicht was du willst. - In Threads, die von erzeugt wurden
async
, ist es für mich etwas unklar, da der Thread möglicherweise wiederverwendet wurde. Nach meinem Verständnis wird nicht garantiert, dass threadlokale Variablen zurückgesetzt werden, aber ich kann mich irren. - Wenn Sie dagegen Ihre eigenen Thread-Pools (mit fester Größe) verwenden, haben Sie die volle Kontrolle, wenn Sie diese wirklich benötigen.
quelle
std::async(launch::async)
scheint jedoch eine viel höhere Chance zu haben, gepoolt zu werden." Nein, ich glaubestd::async(launch::async | launch::deferred)
, das kann zusammengefasst werden. Mit nurlaunch::async
der Aufgabe soll auf einem neuen Thread gestartet werden, unabhängig davon, welche anderen Aufgaben ausgeführt werden. Mit der Richtlinie kannlaunch::async | launch::deferred
die Implementierung dann auswählen, welche Richtlinie, aber was noch wichtiger ist, sie kann die Auswahl der Richtlinie verzögern. Das heißt, es kann warten, bis ein Thread in einem Thread-Pool verfügbar wird, und dann die asynchrone Richtlinie auswählen.std::async()
. Ich bin immer noch gespannt, wie sie nicht triviale thread_local-Destruktoren in einem Thread-Pool unterstützen.launch::async
ist, so behandelt wird, als ob sie nur wärelaunch::deferred
und sie niemals asynchron ausführt. Tatsächlich wählt diese Version von libstdc ++ "aus". immer aufgeschoben zu verwenden, sofern nicht anders erzwungen.std::async
könnte eine schöne Sache für die Leistung gewesen sein - es könnte das Standard-Ausführungssystem für kurzfristige Aufgaben gewesen sein, das natürlich von einem Thread-Pool unterstützt wird. Im Moment ist es nur einstd::thread
Mist, der angeheftet wird, damit die Thread-Funktion einen Wert zurückgeben kann. Oh, und sie haben redundante "verzögerte" Funktionen hinzugefügt, die den Job vonstd::function
vollständig überlappen .Antworten:
Frage 1 :
Ich habe dies gegenüber dem Original geändert, weil das Original falsch war. Ich hatte den Eindruck, dass die Erstellung von Linux-Threads sehr billig war, und nach dem Testen stellte ich fest, dass der Aufwand für den Funktionsaufruf in einem neuen Thread im Vergleich zu einem normalen Thread enorm ist. Der Aufwand für das Erstellen eines Threads zur Verarbeitung eines Funktionsaufrufs ist ungefähr 10000 oder mehr Mal langsamer als bei einem einfachen Funktionsaufruf. Wenn Sie also viele kleine Funktionsaufrufe ausführen, ist ein Thread-Pool möglicherweise eine gute Idee.
Es ist ziemlich offensichtlich, dass die mit g ++ gelieferte Standard-C ++ - Bibliothek keine Thread-Pools hat. Aber ich kann definitiv einen Fall für sie sehen. Selbst mit dem Aufwand, den Anruf durch eine Art Inter-Thread-Warteschlange schieben zu müssen, wäre dies wahrscheinlich billiger als das Starten eines neuen Threads. Und der Standard erlaubt dies.
Meiner Meinung nach sollten die Linux-Kernel-Leute daran arbeiten, die Thread-Erstellung billiger zu machen als derzeit. Die Standard-C ++ - Bibliothek sollte jedoch auch die Verwendung von Pool zur Implementierung in Betracht ziehen
launch::async | launch::deferred
.Und das OP ist korrekt.
::std::thread
Wenn Sie einen Thread starten, wird natürlich die Erstellung eines neuen Threads erzwungen, anstatt einen aus einem Pool zu verwenden. Also::std::async(::std::launch::async, ...)
ist bevorzugt.Frage 2 :
Ja, im Grunde startet dies "implizit" einen Thread. Aber wirklich, es ist immer noch ziemlich offensichtlich, was passiert. Ich denke also nicht, dass das Wort implizit ein besonders gutes Wort ist.
Ich bin auch nicht davon überzeugt, dass es notwendigerweise ein Fehler ist, Sie zu zwingen, vor der Zerstörung auf eine Rückkehr zu warten. Ich weiß nicht, dass Sie den
async
Aufruf verwenden sollten, um 'Daemon'-Threads zu erstellen, von denen nicht erwartet wird, dass sie zurückkehren. Und wenn erwartet wird, dass sie zurückkehren, ist es nicht in Ordnung, Ausnahmen zu ignorieren.Frage 3 :
Persönlich mag ich es, wenn Thread-Starts explizit sind. Ich lege großen Wert auf Inseln, auf denen Sie den seriellen Zugriff garantieren können. Andernfalls erhalten Sie den veränderlichen Status, dass Sie immer irgendwo einen Mutex umwickeln und daran denken müssen, ihn zu verwenden.
Ich mochte das Modell der Arbeitswarteschlange viel besser als das "zukünftige" Modell, da "Inseln der Serien" herumliegen, damit Sie den veränderlichen Zustand effektiver handhaben können.
Aber es kommt wirklich darauf an, was Sie genau tun.
Leistungstest
Also habe ich die Leistung verschiedener Methoden zum Aufrufen von Dingen getestet und diese Nummern auf einem 8-Kern-System (AMD Ryzen 7 2700X) gefunden, auf dem Fedora 29 ausgeführt wird, das mit clang Version 7.0.1 und libc ++ (nicht libstdc ++) kompiliert wurde:
Und nativ, auf meinem MacBook Pro 15 "(Intel (R) Core (TM) i7-7820HQ-CPU bei 2,90 GHz)
Apple LLVM version 10.0.0 (clang-1000.10.44.4)
unter OSX 10.13.6 bekomme ich Folgendes:Für den Arbeitsthread habe ich einen Thread gestartet, dann eine sperrenlose Warteschlange verwendet, um Anforderungen an einen anderen Thread zu senden, und dann darauf gewartet, dass eine Antwort "Es ist erledigt" zurückgesendet wird.
Das "Nichts tun" dient nur zum Testen des Overheads des Testkabels.
Es ist klar, dass der Aufwand für das Starten eines Threads enorm ist. Und selbst der Worker-Thread mit der Warteschlange zwischen den Threads verlangsamt die Arbeit unter Fedora 25 in einer VM um den Faktor 20 und unter nativem Betriebssystem X um etwa 8.
Ich habe ein Bitbucket-Projekt erstellt, das den Code enthält, den ich für den Leistungstest verwendet habe. Es kann hier gefunden werden: https://bitbucket.org/omnifarious/launch_thread_performance
quelle