Forum: PC-Programmierung C++ std::thread und asio


von Asio (Gast)


Lesenswert?

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
1
boost::asio::io_service ioservice;
2
tcp::endpoint endpoint { tcp::v4(), port };
3
tcp::acceptor acceptor { ioservice, endpoint };
4
5
void session(tcp::socket &the_socket)
6
{
7
   // lalala
8
}
9
10
int main()
11
{
12
   for(;;)
13
   {
14
      tcp::socket the_socket(ioservice);
15
      acceptor.accept(the_socket);
16
      std::thread(session, std::move(the_socket).detach();
17
   }
18
}

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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?

von Asio (Gast)


Lesenswert?

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?

von tictactoe (Gast)


Lesenswert?

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).

von Torsten Robitzki (Gast)


Lesenswert?

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

von Asio (Gast)


Lesenswert?

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

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

Asio schrieb:
> Die Schleife wird sicher ziemlich schnell laufen aber gibt dann es eine
> Art Warteschlange bei den eingehenden Verbindungen?
Nein, tut sie nicht, da accept() blockiert, bis eine verbindung zustande 
kommt. :)

http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/basic_socket_acceptor/accept/overload1.html
1
The function call will block until a new connection
2
has been accepted successfully or an error occurs.

Beitrag #5148064 wurde von einem Moderator gelöscht.
von Asio (Gast)


Lesenswert?

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?

von tictactoe (Gast)


Lesenswert?

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.

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

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

von Asio (Gast)


Lesenswert?

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 :-)

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

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.

Beitrag #5149292 wurde von einem Moderator gelöscht.
Beitrag #5149358 wurde von einem Moderator gelöscht.
Beitrag #5149379 wurde von einem Moderator gelöscht.
Beitrag #5149414 wurde von einem Moderator gelöscht.
Beitrag #5153203 wurde von einem Moderator gelöscht.
Beitrag #5153363 wurde von einem Moderator gelöscht.
Beitrag #5153411 wurde von einem Moderator gelöscht.
Beitrag #5153667 wurde von einem Moderator gelöscht.
Beitrag #5153952 wurde von einem Moderator gelöscht.
Beitrag #5158346 wurde von einem Moderator gelöscht.
Beitrag #5158678 wurde von einem Moderator gelöscht.
Beitrag #5158738 wurde von einem Moderator gelöscht.
Beitrag #5159354 wurde von einem Moderator gelöscht.
Beitrag #5161136 wurde von einem Moderator gelöscht.
Beitrag #5161195 wurde von einem Moderator gelöscht.
Beitrag #5162000 wurde von einem Moderator gelöscht.
Beitrag #5165582 wurde von einem Moderator gelöscht.
Beitrag #5165841 wurde von einem Moderator gelöscht.
Beitrag #5166152 wurde von einem Moderator gelöscht.
Beitrag #5167770 wurde von einem Moderator gelöscht.
Beitrag #5167788 wurde von einem Moderator gelöscht.
Beitrag #5167798 wurde von einem Moderator gelöscht.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.