Die Kommentare zum Artikel " Wie man richtig und falsch schläft " haben mich dazu inspiriert, diesen Artikel zu schreiben.
Dieser Artikel konzentriert sich auf die Entwicklung von Multithread-Anwendungen, die Anwendbarkeit von Lock-Free auf einige Fälle, die bei der Arbeit an LAppS auftreten , die Nanosleep- Funktion und Gewalt gegen den Taskplaner .
NB: C++ Linux, POSIX.1-2008 a ( ).
Im Allgemeinen ist alles ziemlich chaotisch, ich hoffe, der Gedankengang in der Präsentation wird klar sein. Bei Interesse bitte ich um eine Katze.
Eventorientierte Software wartet immer auf etwas. Unabhängig davon, ob es sich um eine GUI oder einen Netzwerkserver handelt, warten sie auf Ereignisse: Tastatureingaben, Mauseignisse, Datenpakete, die über das Netzwerk eingehen. Aber jede Software wartet anders. Lock-freie Systeme müssen überhaupt nicht warten. Zumindest sollte die Verwendung von sperrfreien Algorithmen dort erfolgen, wo Sie nicht warten müssen, und sogar schädlich. Wir sprechen jedoch von wettbewerbsfähigen (Multithread-) Systemen, und seltsamerweise warten auch sperrfreie Algorithmen. Ja, sie blockieren nicht die Ausführung paralleler Threads, aber sie selbst warten auf die Gelegenheit, etwas zu tun, ohne zu blockieren.
LAppS verwendet Mutexe und Semaphoren sehr aktiv. Der C ++ - Standard enthält jedoch keine Semaphoren. Der Mechanismus ist sehr wichtig und praktisch, aber C ++ sollte auf Systemen funktionieren, die keine Semaphorunterstützung bieten, und daher sind Semaphoren nicht im Standard enthalten. Wenn ich Semaphoren verwende, weil sie bequem sind, dann Mutexe, weil ich muss.
Das Verhalten des Mutex im Fall von Competitive Lock () wie sem_wait () unter Linux setzt den wartenden Thread am Ende der Taskplaner-Warteschlange. Wenn er sich oben befindet, wird die Prüfung wiederholt, ohne zum Benutzerland zurückzukehren. Wenn der Thread zurück in das Benutzerland zurückkehrt, wird er zurück in die Warteschlange gestellt Das erwartete Ereignis ist noch nicht eingetreten. Dies ist ein sehr wichtiger Punkt.
Und ich habe mich entschlossen zu prüfen, ob ich std :: mutex- und POSIX-Semaphoren ablehnen kann, indem ich sie mit std :: atomic emuliere und die Last hauptsächlich auf userland übertrage. Eigentlich gescheitert, aber das Wichtigste zuerst.
Erstens habe ich mehrere Abschnitte, in denen diese Experimente nützlich sein könnten:
- sperrt in LibreSSL (Fall 1);
- Blockieren beim Übertragen von empfangenen Nutzlastpaketen an Lua-Anwendungen (Fall 2);
- Warten auf Nutzdatenereignisse, die von Lua-Anwendungen verarbeitet werden können (Fall 3).
Beginnen wir mit nicht blockierenden Sperren. Schreiben wir unseren Mutex mit Atomics, wie in einigen Reden von H. Sutter gezeigt (es gibt daher keinen Originalcode aus dem Speicher und daher stimmt der Code nicht mit den ursprünglichen 100% überein, und in Satter war dieser Code mit dem Fortschritt von C ++ 20 verbunden). daher gibt es Unterschiede). Und trotz der Einfachheit dieses Codes gibt es Fallstricke.
#include <atomic> #include <pthread.h> namespace test { class mutex { private: std::atomic<pthread_t> mLock; public: explicit mutex():mLock{0} { } mutex(const mutex&)=delete; mutex(mutex&)=delete; void lock() { pthread_t locked_by=0;
Im Gegensatz zu std :: mutex :: entsperren () ist das Verhalten von test :: mutex: entsperren () beim Versuch, von einem anderen Thread zu entsperren, deterministisch. Eine Ausnahme wird ausgelöst. Dies ist gut, entspricht jedoch nicht dem Standardverhalten. Und was ist schlecht in dieser Klasse? Die schlechte Nachricht ist, dass die test :: mutex: lock () -Methode schamlos CPU-Ressourcen in Zeitkontingenten verbraucht, die dem Thread zugewiesen sind, um den Mutex zu übernehmen, den ein anderer Thread bereits besitzt. Das heißt, Eine Schleife in test :: mutex: lock () verschwendet CPU-Ressourcen. Welche Möglichkeiten haben wir, um diese Situation zu überwinden?
Wir können sched_yield () verwenden (wie in einem der Kommentare zum obigen Artikel vorgeschlagen). Ist es so einfach? Erstens ist es für die Verwendung von sched_yield () erforderlich, dass Ausführungsthreads die Richtlinien SCHED_RR, SCHED_FIFO für ihre Priorisierung im Taskplaner verwenden. Andernfalls wäre der Aufruf von sched_yield () eine Verschwendung von CPU-Ressourcen. Zweitens erhöht ein sehr häufiger Aufruf von sched_yield () den CPU-Verbrauch. Darüber hinaus beschränkt die Verwendung von Echtzeitrichtlinien in Ihrer Anwendung und vorausgesetzt, dass keine anderen Echtzeitanwendungen im System vorhanden sind, die Scheduler-Warteschlange mit der ausgewählten Richtlinie nur auf Ihre Threads. Es scheint, dass das gut ist! Nein, nicht gut. Das ganze System wird weniger reaktionsschnell, weil beschäftigt mit Prioritätsaufgabe. CFQ wird im Stift sein. Es gibt jedoch andere Threads in der Anwendung, und sehr häufig tritt eine Situation auf, wenn der Thread, der den Mutex erfasst hat, am Ende der Warteschlange steht (das Kontingent ist abgelaufen) und der Thread, der darauf wartet, dass der Mutex direkt davor freigegeben wird. In meinen Experimenten (Fall 2) ergab diese Methode ungefähr die gleichen Ergebnisse (3,8% schlechter) wie std :: mutex, aber das System reagiert weniger und der CPU-Verbrauch wird um 5% bis 7% erhöht.
Sie können versuchen, test :: mutex :: lock () wie folgt zu ändern (auch schlecht):
void lock() { pthread_t locked_by=0; while(!mLock.compare_exchange_strong(locked_by,pthread_self())) { static thread_local const struct timespec pause{0,4};
Hier können Sie mit der Schlafdauer in Nanosekunden experimentieren, 4 ns Verzögerungen waren für meine CPU optimal und der Leistungsabfall gegenüber std :: mutex im selben Fall 2 betrug 1,2%. Nicht die Tatsache, dass Nanosleep 4 ns schlief. In der Tat oder mehr (im allgemeinen Fall) oder weniger (wenn unterbrochen). Der Rückgang (!) Des CPU-Verbrauchs betrug 12% -20%. Das heißt, Es war so ein gesunder Traum.
OpenSSL und LibreSSL verfügen über zwei Funktionen, mit denen Rückrufe zum Blockieren eingerichtet werden, wenn diese Bibliotheken in einer Umgebung mit mehreren Threads verwendet werden. Es sieht so aus:
// callback void openssl_crypt_locking_function_callback(int mode, int n, const char* file, const int line) { static std::vector<std::mutex> locks(CRYPTO_num_locks()); if(n>=static_cast<int>(locks.size())) { abort(); } if(mode & CRYPTO_LOCK) locks[n].lock(); else locks[n].unlock(); } // callback-a CRYPTO_set_locking_callback(openssl_crypt_locking_function_callback); // id CRYPTO_set_id_callback(pthread_self);
Und jetzt ist das Schlimmste, dass die Verwendung des obigen Tests :: mutex mutex in LibreSSL die Leistung von LAppS um fast das Zweifache reduziert. Unabhängig von der Option (leere Warteschleife, sched_yield (), nanosleep ()).
Im Allgemeinen löschen wir Fall 2 und Fall 1 und bleiben bei std :: mutex.
Kommen wir zu den Semaphoren. Es gibt viele Beispiele für die Implementierung von Semaphoren mit std :: condition_variable. Sie alle verwenden ebenfalls std :: mutex. Und solche Semaphorsimulatoren sind (nach meinen Tests) langsamer als POSIX-Systemsemaphoren.
Deshalb werden wir ein Semaphor über Atome machen:
class semaphore { private: std::atomic<bool> mayRun; mutable std::atomic<int64_t> counter; public: explicit semaphore() : mayRun{true},counter{0} { } semaphore(const semaphore&)=delete; semaphore(semaphore&)=delete; const bool post() const { ++counter; return mayRun.load(); } const bool try_wait() { if(mayRun.load()) { if(counter.fetch_sub(1)>0) return true; else { ++counter; return false; } }else{ throw std::system_error(ENOENT,std::system_category(),"Semaphore is destroyed"); } } void wait() { while(!try_wait()) { static thread_local const struct timespec pause{0,4}; nanosleep(&pause,nullptr); } } void destroy() { mayRun.store(false); } const int64_t decrimentOn(const size_t value) { if(mayRun.load()) { return counter.fetch_sub(value); }else{ throw std::system_error(ENOENT,std::system_category(),"Semaphore is destroyed"); } } ~semaphore() { destroy(); } };
Oh, dieses Semaphor ist um ein Vielfaches schneller als das Systemsemaphor. Das Ergebnis eines separaten Tests dieses Semaphors mit einem Anbieter und 20 Verbrauchern:
OS semaphores test. Started 20 threads waiting on a semaphore Thread(OS): wakes: 500321 Thread(OS): wakes: 500473 Thread(OS): wakes: 501504 Thread(OS): wakes: 502337 Thread(OS): wakes: 498324 Thread(OS): wakes: 502755 Thread(OS): wakes: 500212 Thread(OS): wakes: 498579 Thread(OS): wakes: 499504 Thread(OS): wakes: 500228 Thread(OS): wakes: 499696 Thread(OS): wakes: 501978 Thread(OS): wakes: 498617 Thread(OS): wakes: 502238 Thread(OS): wakes: 497797 Thread(OS): wakes: 498089 Thread(OS): wakes: 499292 Thread(OS): wakes: 498011 Thread(OS): wakes: 498749 Thread(OS): wakes: 501296 OS semaphores test. 10000000 of posts for 20 waiting threads have taken 9924 milliseconds OS semaphores test. Post latency: 0.9924ns ======================================= AtomicEmu semaphores test. Started 20 threads waiting on a semaphore Thread(EmuAtomic) wakes: 492748 Thread(EmuAtomic) wakes: 546860 Thread(EmuAtomic) wakes: 479375 Thread(EmuAtomic) wakes: 534676 Thread(EmuAtomic) wakes: 501014 Thread(EmuAtomic) wakes: 528220 Thread(EmuAtomic) wakes: 496783 Thread(EmuAtomic) wakes: 467563 Thread(EmuAtomic) wakes: 608086 Thread(EmuAtomic) wakes: 489825 Thread(EmuAtomic) wakes: 479799 Thread(EmuAtomic) wakes: 539634 Thread(EmuAtomic) wakes: 479559 Thread(EmuAtomic) wakes: 495377 Thread(EmuAtomic) wakes: 454759 Thread(EmuAtomic) wakes: 482375 Thread(EmuAtomic) wakes: 512442 Thread(EmuAtomic) wakes: 453303 Thread(EmuAtomic) wakes: 480227 Thread(EmuAtomic) wakes: 477375 AtomicEmu semaphores test. 10000000 of posts for 20 waiting threads have taken 341 milliseconds AtomicEmu semaphores test. Post latency: 0.0341ns
Dieses Semaphor mit fast freiem Post (), das 29-mal schneller als das System ist, weckt auch die darauf wartenden Threads sehr schnell auf: 29325 Aufwecken pro Millisekunde gegenüber 1007 Aufwecken pro Millisekunde vom System. Es hat deterministisches Verhalten mit einem zerstörten Semaphor oder einem zerstörbaren Semaphor. Und natürlich Segfault beim Versuch, einen bereits zerstörten zu verwenden.
(¹) Tatsächlich kann ein Stream so oft in einer Millisekunde nicht vom Scheduler verzögert und geweckt werden. Weil post () blockiert nicht. In diesem synthetischen Test befindet sich wait () sehr oft in einer Situation, in der Sie nicht schlafen müssen. Gleichzeitig lesen mindestens 7 Threads parallel den Wert des Semaphors.
Die Verwendung in Fall 3 in LAppS führt jedoch unabhängig von der Schlafzeit zu Leistungsverlusten. Er wacht zu oft auf, um dies zu überprüfen, und Ereignisse in LAppS kommen viel langsamer an (Netzwerklatenz, Latenz der Clientseite, die die Last erzeugt, usw.). Weniger häufig zu prüfen bedeutet auch Leistungseinbußen.
Darüber hinaus ist die Verwendung von Schlaf in solchen Fällen und auf ähnliche Weise völlig schädlich, weil Auf einer anderen Hardware können sich die Ergebnisse als völlig anders herausstellen (wie im Fall der Assembler-Anweisungspause), und für jedes CPU-Modell müssen Sie auch die Verzögerungszeit auswählen.
Der Vorteil eines Systemmutex und -semaphors besteht darin, dass der Ausführungsthread erst dann aktiviert wird, wenn ein Ereignis (Entsperren des Mutex oder Inkrementieren des Semaphors) auftritt. Zusätzliche CPU-Zyklen werden nicht verschwendet - Gewinn.
Im Allgemeinen bringt alles von diesem bösen, das Deaktivieren von iptables auf meinem System von 12% (mit TLS) bis 30% (ohne TLS) einen Leistungsgewinn ...