Hallo,
ich habe hier ein Codebeispiel für ein Serverprogramm, welches
Verbindungen annimmt und jeweils einen Thread dazu startet. Das
funktioniert zwar alles super aber ich möchte es auch verstehen.
Meine Frage betrifft das Erstellen der Threads. Threads sind mir bekannt
aber normalerweise habe ich Threads mit einer Threadvariable gestartet
zB std::thread variable(funktion); oder als Threadpool im vector.
Im untenstehenden Code gibt es keine, ich nenne es mal "Threadvariable".
Diese Threadvariable ist eigentlich ganz nützlich um mit joinable() zu
überprüfen ob der Thread existiert oder die ID abzufragen.
Kann mir jemand erklären wie es funktioniert, dass immer das Gleiche
aufgerufen und übergeben wird und damit trotzdem unterschiedliche
Threads erzeugt werden? Auch das std::move(the_socket) ist mir nicht
ganz klar, es wird immer das Gleiche übergeben also immer das
Socketobjekt.
Kann man ohne "Threadvariable" so die Threads noch identifizieren? Und
kann man die Sockets identifizieren?
Grüße
Asio schrieb:> Diese Threadvariable ist eigentlich ganz nützlich um mit joinable() zu> überprüfen ob der Thread existiert oder die ID abzufragen.
In einer Multi-Threaded Umgebung ist so ein Überprüfung relativ sinnlos,
da sobald Du die Information bekommen hast, die Information schon wieder
falsch sein kann.
> Kann mir jemand erklären wie es funktioniert, dass immer das Gleiche> aufgerufen und übergeben wird und damit trotzdem unterschiedliche> Threads erzeugt werden? Auch das std::move(the_socket) ist mir nicht> ganz klar, es wird immer das Gleiche übergeben also immer das> Socketobjekt.
Der Beispiel-Server erzeugt mit jedem Schleifen-Durchlauf zuerste einen
socket, dann einen thread, nach dem eine Verbindung angenommen wurde.
Der thread wird detached gestartet, dass bedeutet, dass der thread
sobald er beendet ist, alle resourcen frei gibt.
> Kann man ohne "Threadvariable" so die Threads noch identifizieren? Und> kann man die Sockets identifizieren?
Nein, aber warum würdest Du das wollen?
Danke für die Erklärung. Mir ist noch eingefallen, die Sache mit der
Thread-ID kann man auch in der Threadfunktion abfragen per
this_thread::get_id().
Was ich noch nicht verstehe ist, dass the_socket ein mal ausserhalb der
Threads erzeugt wird und dann, so verstehe ich es, mit std::move
irgendwie in/an die Funktion übergeben wird.(mit std::move hatte ich bis
hier leider noch keine Erfahrung).
Beim nächsten Schleifendurchlauf wird wieder the_socket erzeugt und
anschließend in die Funktion übergeben.
Wie wird denn garantiert das mit jedem Erzeugen von the_socket auch ein
wirklich neuer Socket erzeugt wird? (daher die Frage nach der
identifizierung). Und das jeweils durch std::move "verschobene"
the_socket muss, wenn ich es mal wie eine Variable betrachte, doch
irgend einen Speicherort haben. In der Funktion wird über die Referenz
darauf zugegriffen, das heißt in der Funktion befindet es sich wohl
nicht. Daher frage ich mich, haben alle Threads bzw die als Thread
geöffneten Funktionsausführungen irgendwo ein eigenen Speicherort für
the_socket?
Asio schrieb:> Wie wird denn garantiert das mit jedem Erzeugen von the_socket auch ein> wirklich neuer Socket erzeugt wird?
Das geschieht schon allein deshalb, weil die Variable the_socket am
Anfang des Schleifenkörpers definiert ist, also neu angelegt wird; am
Ende des Schleifenkörpers wird die variable wieder zerstört. Das tut
aber dem Socket nichts zuleide, weil der mittlerweile verschoben worden
ist.
Asio schrieb:> Und das jeweils durch std::move "verschobene"> the_socket muss, wenn ich es mal wie eine Variable betrachte, doch> irgend einen Speicherort haben. In der Funktion wird über die Referenz> darauf zugegriffen, das heißt in der Funktion befindet es sich wohl> nicht. Daher frage ich mich, haben alle Threads bzw die als Thread> geöffneten Funktionsausführungen irgendwo ein eigenen Speicherort für> the_socket?
Dieser Speicherort befindet sich im irgendwo im Hintergrund, wo
std::thread einen angelegt hat. Dieser wird befüllt, indem the_socket
dort hinein verschoben wird (wegen std::move).
Asio schrieb:> Was ich noch nicht verstehe ist, dass the_socket ein mal ausserhalb der> Threads erzeugt wird und dann, so verstehe ich es, mit std::move> irgendwie in/an die Funktion übergeben wird.(mit std::move hatte ich bis> hier leider noch keine Erfahrung).
Move-Semantik ist ähnlich zu einem Swap. Die ursprüngliche Idee war
keine teuren Kopien von temporären Objekten zu machen, wenn man weis,
dass diese Objekte eh gleich wieder zerstört werden. Statt dessen
werden direkt ihre teuer konstruierten internen Zustände übernommen und
die temporären Objekte in einem "leeren" Zustand hinterlassen. Google
mal nach "Move-Semantik" und "rvalue reference".
Semantisch ist das in etwa so, wie ein Zeiger auf ein dynamisch
allokiertes Objekt, dessen Quelle nach einer Kopie auf null gesetzt
wird. Das ist hilfreich, wenn man Objekte hat, die Semantisch nicht
kopierbar sein sollen (socket, thread, file etc.), die man aber trotzdem
bewegen möchte.
> Beim nächsten Schleifendurchlauf wird wieder the_socket erzeugt und> anschließend in die Funktion übergeben.
Genau, der Scope von `the_socket` fängt mit dem body der Schleife an,
und endet am Ende des Blocks. Es wird ein nicht verbundener Socket
erzeugt, dann blockt die Schleife auf `acceptor.accept`, bis eine
Verbindung angenommen wird. Dann ist `the_socket` ein verbundener Socket
der in den c'tor des anonymen threads bewegt wird. Danach ist
`the_socket` nur noch eine leere Hülle, dessen Destruktor keine
Seiteneffekte (wie das Schließen des Sockets) hat.
> Wie wird denn garantiert das mit jedem Erzeugen von the_socket auch ein> wirklich neuer Socket erzeugt wird?
Das mit jedem Schleifendurchlauf eine neue `the_socket` Variable
angelegt wird, garantiert Dir die Sprache. Es wird Speicher auf dem
Stack reserviert und danach der Constructor aufgerufen. Am Ende der
Schleife wird der d'tor aufgerufen.
> (daher die Frage nach der> identifizierung). Und das jeweils durch std::move "verschobene"> the_socket muss, wenn ich es mal wie eine Variable betrachte, doch> irgend einen Speicherort haben.
Das macht std::thread. std::thread kopiert (bzw. moved) die c'tor
argumente (z.B. in den heap oder auf den stack des neuen thread context)
und ruft dann mit `session()` auf.
> Daher frage ich mich, haben alle Threads bzw die als Thread> geöffneten Funktionsausführungen irgendwo ein eigenen Speicherort für> the_socket?
Yep (s.o.)
mfg Torsten
Super danke, selten an einem Tag so viel Neues gelernt.
Ich verstehe es jetzt so, mal ganz einfach formuliert, std::move klaut
sozusagen den Pointer von einen fremden Datenbereich um ihn weiter zu
benutzen, in dem Falle im Thread.
Mal abgesehen davon, die Sache mit den rvalues ist sicher auch für
Performanceoptimierungen spannend.
Torsten Robitzki schrieb:> Dann ist `the_socket` ein verbundener Socket> der in den c'tor des anonymen threads bewegt wird. Danach ist> `the_socket` nur noch eine leere Hülle, dessen Destruktor keine> Seiteneffekte (wie das Schließen des Sockets) hat.
Ist die Lebenszeit von "the_socket" ist dann verlässlich den gesamten
Thread bzw für den gesamten Durchlauf der Funktion gewährleißtet?
Ist der gezeigte Code eigentlich in Ordnung für Situationen wo
beispielsweise gleichzeitig sehr viele Verbindungen von Clients
eingehen?
Die Schleife wird sicher ziemlich schnell laufen aber gibt dann es eine
Art Warteschlange bei den eingehenden Verbindungen?
Grüße
Kaj G. schrieb:> Nein, tut sie nicht, da accept() blockiert, bis eine verbindung zustande> kommt. :)
Daher schrieb ich
Asio schrieb:> Situationen wo> beispielsweise gleichzeitig sehr viele Verbindungen von Clients> eingehen
Wenn sehr viele Verbindungen kommen sollte accept() nicht blockieren da
ja immer ein nächster verbinden will und so die Schleife schnell
durchlaufen sollte. Die Frage lief eher darauf hinaus wenn rein fiktiv
angenommen 1000 Clients genau gleichzeitig verbinden wollen, werden dann
alle sauber mit obigem Code angenommen? Bevor accept() überhaupt
aufgerufen wird, würden so schon etliche Clients warten. Wie wird
entschieden welcher Client als nächstes "accepted" wird wenn so viele
warten?
Hinter acceptor.accept() muss sich irgendwo ein Aufruf der
Betriebsystem-Funktion listen() verbergen, und die hat einen Parameter
backlog, der die Größe des Wartebereichs angibt. Jetzt muss nur mehr die
Anwendung schnell genug accept() aufrufen, damit sich der Wartebereich
nicht füllt.
Asio schrieb:> Die Frage lief eher darauf hinaus wenn rein fiktiv> angenommen 1000 Clients genau gleichzeitig verbinden wollen,
Ist ja schoen, dass die Clients das wollen. Bevor aber eine Verbindung
zustande kommt, kommen da erst noch so sachen wie Netzwerkprotokoll und
Routing. Und da wird es dann schon schwierig mit dem "genau
gleichzeitig".
tictactoe schrieb:> damit sich der Wartebereich> nicht füllt.
Du meinst, damit er nicht ueberlaeuft. Ein Wartebereich ist ja da, damit
er sich fuellen kann, er darf nur nicht zu voll werden. :P
ok prima, dann sollte es kein Problem geben wenn sich das sowieso von
selbst reguliert, eine Sache weniger um die man sich kümmern muss, umso
besser :-)
Asio schrieb:> Die Frage lief eher darauf hinaus wenn rein fiktiv> angenommen 1000 Clients genau gleichzeitig verbinden wollen, werden dann> alle sauber mit obigem Code angenommen?
Wenn Du 1000 Clients parallel bedienen möchtest, würdest Du aber lieber
nicht die "1 thread pro client" Architektur verwenden wollen.