Warte auf eine Bedingung (pthread_cond_wait) und einen Socketwechsel (select) gleichzeitig

8

Ich schreibe einen POSIX-kompatiblen Multi-Thread-Server in c / c ++, der in der Lage sein muss, eine große Anzahl von Verbindungen asynchron anzunehmen, zu lesen und zu schreiben. Der Server hat mehrere Worker-Threads, die Aufgaben ausführen und gelegentlich (und unvorhersehbar) Daten in die Warteschlangen schreiben, die in die Sockets geschrieben werden. Daten werden auch gelegentlich (und unvorhersehbar) von den Clients in die Sockets geschrieben, daher muss der Server auch asynchron lesen. Eine naheliegende Möglichkeit, dies zu tun, besteht darin, jeder Verbindung einen Thread zu geben, der von / zu ihrem Socket liest und schreibt; Dies ist jedoch hässlich, da jede Verbindung für eine lange Zeit bestehen kann und der Server daher möglicherweise hundert oder tausend Threads speichern muss, um die Verbindungen zu verfolgen.

Ein besserer Ansatz wäre ein einzelner Thread, der alle Kommunikationen mit den Funktionen select () / pselect () behandelt. D. h., Ein einzelner Thread wartet auf jeden Socket, um lesbar zu sein, und erzeugt dann einen Job, um die Eingabe zu verarbeiten, die von einem Pool anderer Threads behandelt wird, wenn Eingaben verfügbar sind. Immer wenn die anderen Worker-Threads die Ausgabe für eine Verbindung produzieren, wird sie in die Warteschlange gestellt, und der Kommunikationsthread wartet darauf, dass dieser Socket beschreibbar ist, bevor er geschrieben wird.

Das Problem dabei ist, dass der Kommunikationsthread möglicherweise in der Funktion select () oder pselect () wartet, wenn die Ausgabe von den Arbeitsthreads des Servers in die Warteschlange gestellt wird. Es ist möglich, dass, wenn keine Eingabe für einige Sekunden oder Minuten eintrifft, ein wartender Chunk der Ausgabe nur darauf wartet, dass der Kommunikationsthread ausgeführt wird. Dies sollte jedoch nicht passieren - Daten sollten so schnell wie möglich geschrieben werden.

Im Moment sehe ich ein paar Lösungen, die Thread-sicher sind. Einer ist, den Kommunikationsthread beschäftigt zu haben - warten auf die Eingabe und aktualisieren die Liste der Sockets, auf die er wartet, um jede Zehntelsekunde oder so zu schreiben. Dies ist nicht optimal, da es busy-waiting beinhaltet, aber es wird funktionieren. Eine weitere Option ist die Verwendung von pselect () und das Senden des USR1-Signals (oder etwas Gleichwertiges), wenn neue Ausgaben in die Warteschlange gestellt wurden, sodass der Kommunikations-Thread die Liste der Sockets, die auf den Schreibstatus warten, sofort aktualisieren kann. Ich bevorzuge das letztere hier, aber ich mag es immer noch nicht, ein Signal für etwas zu verwenden, das eine Bedingung sein sollte (pthread_cond_t). Eine weitere Option wäre, in die Liste der Dateideskriptoren, auf die select () wartet, eine Dummy-Datei aufzunehmen, in die wir ein einzelnes Byte schreiben, wenn ein Socket zum schreibbaren fd_set für select () hinzugefügt werden muss; Dies würde den Kommunikationsserver aufwecken, da diese bestimmte Dummy-Datei dann lesbar wäre, wodurch der Kommunikations-Thread sofort seinen schreibbaren fd_set aktualisieren könnte.

Ich fühle intuitiv, dass der zweite Ansatz (mit dem Signal) der "richtigste" Weg ist, den Server zu programmieren, aber ich bin neugierig, ob irgendjemand weiß, welches der oben genannten am effizientesten ist, allgemein gesagt, ob Beide oben genannten Punkte führen zu Wettlaufbedingungen, die mir nicht bekannt sind, oder wenn jemand eine allgemeinere Lösung für dieses Problem kennt. Was ich wirklich will, ist eine Funktion pthread_cond_wait_and_select (), die es dem Comm-Thread erlaubt, sowohl auf eine Änderung an Sockets als auch auf ein Signal von einer Bedingung zu warten.

Vielen Dank im Voraus.

    
user1110198 21.12.2011, 16:23
quelle

3 Antworten

6

Dies ist ein ziemlich häufiges Problem.

Eine häufig verwendete Lösung besteht darin, Pipes als Kommunikationsmechanismus von Worker-Threads zurück zum I / O-Thread zu haben. Nach Beendigung seiner Aufgabe schreibt ein Worker-Thread den Zeiger auf das Ergebnis in die Pipe. Der E / A-Thread wartet auf das Leseende der Pipe zusammen mit anderen Sockets und Dateideskriptoren und sobald die Pipe zum Lesen bereit ist, wird sie wieder aktiviert, ruft den Zeiger auf das Ergebnis ab und fährt mit dem Drücken des Ergebnisses in die Clientverbindung in non fort -Blockierungsmodus.

Beachten Sie, dass Pipe Lese- und Schreibvorgänge von weniger als oder gleich PIPE_BUF atomar sind, die Zeiger werden in einem Schuss geschrieben und gelesen. Aufgrund der Atomaritätsgarantie können sogar mehrere Worker-Threads Zeiger in dieselbe Pipe schreiben.

    
Maxim Egorushkin 21.12.2011, 17:05
quelle
3

Ihr zweiter Ansatz ist der sauberere Weg zu gehen. Es ist völlig normal, dass Dinge wie select oder epoll benutzerdefinierte Ereignisse in Ihre Liste aufnehmen. Dies ist, was wir in meinem aktuellen Projekt tun, um solche Ereignisse zu behandeln. Wir verwenden auch Timer (für Linux timerfd_create ) für periodische Ereignisse.

Unter Linux können Sie mit der eventfd solche willkürlichen Benutzerereignisse für diesen Zweck erstellen - also würde ich sagen, dass dies durchaus akzeptiert ist. Für POSIX nur Funktionen, naja, hmm, vielleicht einen der Pipe-Befehle oder socketpair habe ich auch gesehen.

Busy-Polling ist keine gute Option. Zuerst scannen Sie den Speicher, der von anderen Threads verwendet wird, und verursachen so einen CPU-Speicherkonflikt. Zweitens müssen Sie immer zu Ihrem select -Aufruf zurückkehren, der eine große Anzahl von Systemaufrufen und Kontextwechsel erzeugt, die die gesamte Systemleistung beeinträchtigen.

    
edA-qa mort-ora-y 21.12.2011 16:40
quelle
3

Leider ist der beste Weg, dies zu tun, für jede Plattform anders. Die kanonische, portable Methode ist es, Ihren I / O-Thread-Block in poll zu haben. Wenn Sie möchten, dass der E / A-Thread poll belässt, senden Sie ein einzelnes Byte an einem pipe , das vom Thread abgefragt wird. Dadurch wird der Thread sofort von poll beendet.

Unter Linux ist epoll der beste Weg. Auf BSD-basierten Betriebssystemen (einschließlich OSX, denke ich), kqueue . Unter Solaris war es /dev/poll und es gibt jetzt noch etwas anderes, dessen Namen ich vergesse.

Vielleicht möchten Sie nur eine Bibliothek wie libevent oder Boost.Asio . Sie bieten Ihnen das beste E / A-Modell auf jeder Plattform, die sie unterstützen.

    
David Schwartz 22.12.2011 03:03
quelle