wir sind hier an einem Server-Projekt unter Linux.
Hierzu arbeiten wir mit vielen Threads und demzufolge mit zahlreichen
Mutexes.
Die Planungen haben einen Bedarf von 5000+ Mutexes ergeben.
Hat irgendjemand Erfahrung wie sich der Kernel bei einer derart großen
Anzahl verhält.
Rein theoretisch müsste das ja klappen, zumal unter Posix ein Mutex keine
weiteren Ressourcen (außer Speicher für ihn selbst) benötigt.
Über Erfahrungen aus der "realen Welt" würde ich mich sehr freuen. Nicht
dass der Kernel platzt :-)))
Viele Grüße,
Marco Keuthen
phenomic game-design
Den Kernel interessiert das generell nicht,
da ein Thread-Mutex keine Kernelresource ist.
Wenn du wirklich mehrere Threads brauchst, dann
schaue dir dafuer passende Designpattern an
z.B. http://www.cs.wustl.edu/~schmidt/ACE.html
Oder, wie hier in der Newsgroup oft vertreten,
verzichte ganz auf Threads und benutze eintsprechende
Kombinationen aus select/poll + Callback mit
event. mehreren Prozessen.
Alles andere ist der sichere Weg in die Klapsmuehle :-)
-Arne
"Server" und "viele Threads" beißen sich.
Denn bei Servern ist Skalierbarkeit wichtig, wobei sich gleichzeitig
die Anzahl der Threads in Grenzen hält.
> und demzufolge mit zahlreichen Mutexes.
> Die Planungen haben einen Bedarf von 5000+ Mutexes ergeben.
Uff. Was soll das Ding eigentlich darstellen, wenn es fertig ist?
Ich frag nur, weil mir 5000 so oder so _zu_ viel vorkommt.
Und Du weißt hoffentlich eh: Je mehr Threads, Mutexes, etc. Du
verwendest, desto schwerer tust Du Dich beim Debuggen. Besonders dann,
wenn Du eine Race Condition hast, die nur "manchmal" unter starker
Belastung auftritt...
> Rein theoretisch müsste das ja klappen, zumal unter Posix ein Mutex
> keine weiteren Ressourcen (außer Speicher für ihn selbst) benötigt.
Jup. Vom Kernel her wird AFAIK nichts weiter benötigt.
--
No sig @ work.
ich habe mich ein wenig unklar ausgedrück. Sorry.
Es handelt sich um einen UDP Server für ein Projekt, der mit sehr vielen
"Clients" im Internet kommuniziert.
> Uff. Was soll das Ding eigentlich darstellen, wenn es fertig ist?
> Ich frag nur, weil mir 5000 so oder so _zu_ viel vorkommt.
5000 ist ja auch nur ein rein theoretischer Wert - bei 5000 Connections und
jeweils einer Mutex pro Connections.
Die Auflösung pro Connection macht evtl. Sinn, wenn man prüfen muss, welche
Connection gerade irgendwas gesperrt / im Zugriff hat.
Evtl. macht es auch Sinn, eine Mutex für z.B. x-Connections zu verwenden.
(wobei X = 5, 8, 16, 32 oder 64, ...)
> > Rein theoretisch müsste das ja klappen, zumal unter Posix ein Mutex
> > keine weiteren Ressourcen (außer Speicher für ihn selbst) benötigt.
>
> Jup. Vom Kernel her wird AFAIK nichts weiter benötigt.
Wollte halt nur wissen, ob jemand schon Erfahrungen mit _vielen_ Mutexes
gemacht hat :-)
Gruß,
Marco
Das schreit ja schon fast nach einer Event-getriebenen Abarbeitung.
D.h., man hat (wenn überhaupt) nur zwei Threads: Einer horcht auf dem
UDP-Port und trägt die empfangene Message zusammen mit der Sendeadresse
in eine Event-Queue ein. Und ein zweiter Thread arbeitet diese Queue
Message für Message ab. Daraus folgt, daß Du nur einen Mutex brauchst,
mit dem das Einfügen/Auslesen der Event-Queue synchronisiert wird.
Genaugenommen läßt sich das ganze auch in einem einzigen Prozess lösen.
D.h., man kommt, wenn es sein muß, auch ohne Threads aus.
Das hängt dann letztendlich von Deinen Anforderungen ab.
Wenn z.B. das Lesen vom UDP-Socket "schnell" gehen muß und das
Abarbeiten ruhig etwas verzögert sein kann, dürfte der Ansatz mit zwei
Threads passend sein. Wenn sich die Anzahl der "gleichzeitig"
eintreffenden Pakete in Grenzen hält, kann der Ansatz ohne Threads
verwendet werden: Lesen - Bearbeiten - Zurücksenden.
> 5000 ist ja auch nur ein rein theoretischer Wert - bei 5000
> Connections und jeweils einer Mutex pro Connections.
Häh? Bei UDP gibts keine Connections.
Es sei denn, Du verstehst unter dem Begriff "Connection" irgend
etwas anderes, als ich... :-)
Und der Useless Use of Multithreading Award geht an... Bernhard Trummer,
für seine eloquente Selbstdemontage in de.comp.us.unix.programming in
<3b836fe4$0$21924$6e36...@newsreader02.highway.telekom.at>.
> Genaugenommen läßt sich das ganze auch in einem einzigen Prozess lösen.
> D.h., man kommt, wenn es sein muß, auch ohne Threads aus.
Meine Güte, du hast das ja wirklich _überhaupt_ nicht verstanden...?!
Bevor du hier anderen Anfängern Threads empfiehlst, solltest du dich
wirklich mal mit der rudimentären Nomenklatur des Gebietes beschäftigen.
Felix
Wenn Du anstatt nur rumzumaulen auch noch schreiben würdest, was genau
Dich stört und wie es besser machen könnte, fände ich deine Postings noch
aufschlussreicher.
Gruss,
Daniel
Wenn du das nicht weißt, kann (und will) ich dir nicht helfen.
Ich helfe nur Leuten, die selbständig recherchiert haben und rudimentäre
Kenntnisse im fraglichen Themenkreis haben.
Ich lese dir das Handbuch auch gerne persönlich vor. Kostet dich
lächerliche 20 Kilomark pro Seite. Plus Spesen, natürlich.
Felix
Könntest du kurz sagen, wo ich das nachlesen kann?
Ich dachte zwar ich hätte es verstanden, allerdings bin ich durch deine
Aussagen verunsichert. Ich weiss nich, ob ich das Thema oder deine
Aussage nicht verstanden hab :)
> Ich lese dir das Handbuch auch gerne persönlich vor. Kostet dich
> lächerliche 20 Kilomark pro Seite. Plus Spesen, natürlich.
Von welchem Handbuch redest du genau?
Sorry, aber das würde mir sehr helfen ...
Leider liegt mir das Buch von Stevens noch nicht vor...
Christian Dickmann,
der um verzeihung bittet für seine Unwissenheit (Verunsichertheit),
aber eine klare Aussage von dir wäre echt hilfreicher (bzw. ein "Link"
zum selber nachlesen wäre toll!)
Im Stevens z.B.
Oder in allen anderen Büchern über Netzwerkprogrammierung/IPC unter Unix.
> > Ich lese dir das Handbuch auch gerne persönlich vor. Kostet dich
> > lächerliche 20 Kilomark pro Seite. Plus Spesen, natürlich.
> Von welchem Handbuch redest du genau?
Wenn du mir 20 Kilomark pro Seite zahlst, ist mir egal, welches Handbuch
du vorgelesen haben willst.
> Leider liegt mir das Buch von Stevens noch nicht vor...
man 2 select
man 2 poll
Felix
Stevens will ich mir die ganze zeit schon kaufen ...
Ich denke innerhalb der nächsten woche wird es in meinem regal stehen :)
> > Leider liegt mir das Buch von Stevens noch nicht vor...
>
> man 2 select
> man 2 poll
danke.
Christain Dickmann
Nein, ich weiss nicht was _Dich_ stört, da ich kein Hellseher bin.
> Ich helfe nur Leuten, die selbständig recherchiert haben und rudimentäre
> Kenntnisse im fraglichen Themenkreis haben.
Meinen Reim kann ich mir selbst drauf machen, aber wer andere Beiträge
bewertet, sollte dann auch konkret auf Fehler hinweisen und evtl.
Verbesserungen anbringen.
> Ich lese dir das Handbuch auch gerne persönlich vor. Kostet dich
> lächerliche 20 Kilomark pro Seite. Plus Spesen, natürlich.
Hahaha, ich lach micht tot. Du warst früher bestimmt der Klassenclown.
Daniel
Ich weiß was select() und poll() ist. Ich bin ja schließlich kein
Anfänger, so wie Du mich hinzustellen versuchst...
OK, warum ich eine Methode mit Threads vorgeschlagen habe, will ich
anhand folgendem Szenario versuchen zu beschreiben:
Ich habe ein "Kasterl", das u.a. einen Server zur Verfügung stellt
und folgende Eigenschaften besitzt:
* Er soll auf "beliebig viele" Sockets (Verbindungen von Clients)
lauschen können => Einsatz von select() oder poll() -- was sonst.
* Jede auf einem Socket empfangene Message wird bearbeitet und eine
Antwortmessage wird generiert, welche dann auf dem gleichen Socket
zurückgesendet wird.
* Die Bearbeitung von den empfangenen Messages und das Generieren der
Antwortmessage braucht jedoch rel. viel CPU-Zeit, da ASN.1 De- und
Encodierungen und diverse Crypto-Verfahren (Ver- und Entschlüsselung,
Hashwerte, etc.) im Spiel sind.
Möglichkeit 1:
Man macht alles in einem Prozess -- ohne Threads.
Vorteil: Einfach, da keine interne Synchronisation notwendig.
Nachteil: Bei starker (burstartiger) Belastung kann es passieren,
daß ich z.B. von einem Client mehr Messages empfange, als der Prozess
abarbeiten kann. D.h., die Empfangs-Queue vom TCP/IP Stack wird
irgendwann voll sein und ich verliere Pakete.
Möglichkeit 2:
Ich mache zwei Threads. Einer macht den select() und schreibt alle
empfangenen Messages in eine Event-Liste, ohne die oben genannten
Bearbeitungsschritte durchzuführen. Der zweite Thread arbeitet diese
Liste Eintrag für Eintrag ab und sendet die Antwortpakete zurück zum
Client.
Vorteil: Die Empfangs-Queue vom TCP/IP Stack kann "schneller"
ausgelesen werden, wodurch ich bei höherer Last weniger bzw. gar
keine Paketverluste habe.
Nachteil: Wenn der Rechner zu "schwach" ist, treten bei hoher
Belastung Verzögerungen auf, da immer nur eine Message nach der
anderen bearbeitet werden kann. Mit diesem Nachteil kann ich jedoch
leben, solange kein Paket "verloren geht".
BTW, ich weiß, daß man mit setsockopt() die Größe des receive buffers
anpassen kann. Da ich jedoch nicht wirklich eine obere Schranke des
receive buffers vorherbestimmen kann, finde ich, daß ich mit der
zweiten Möglichkeit flexibler unterwegs bin.
Falls Du eine dritte Möglichkeit auf Lager hast, die meinetwegen
ohne Threads auskommt und meine Anforderungen erfüllt, dann laß
bitte hören. Ach ja, daß ganze Ding wird zwar für Linux bzw. AiX
entwickelt. Jedoch soll es unter WinNT auch funktionieren, was
bedeutet, daß man viele der netten Unix-Spielereien (z.B. fork(),
socketpair(), etc.) nicht verwenden kann. :-(
Ich weiß was select() und poll() ist. Ich bin ja schließlich kein
Anfänger, so wie Du mich hinzustellen versuchst...
OK, warum ich eine Methode mit Threads vorgeschlagen habe, will ich
anhand folgendem Szenario versuchen zu beschreiben:
Ich habe ein "Kasterl", das u.a. einen Server zur Verfügung stellt
und folgende Eigenschaften besitzt:
* Er soll auf "beliebig viele" UDP-Sockets lauschen können
--
No sig @ work.
Das ist FvL, seine Kommentare kannst Du getrost ignorieren. Sie enthalten
selten echte Information.
Bernhard Trummer <bernhard...@gmx.at> wrote:
> Möglichkeit 2:
> Ich mache zwei Threads. Einer macht den select() und schreibt alle
> empfangenen Messages in eine Event-Liste, ohne die oben genannten
> Bearbeitungsschritte durchzuführen. Der zweite Thread arbeitet diese
> Liste Eintrag für Eintrag ab und sendet die Antwortpakete zurück zum
> Client.
> Vorteil: Die Empfangs-Queue vom TCP/IP Stack kann "schneller"
> ausgelesen werden, wodurch ich bei höherer Last weniger bzw. gar
> keine Paketverluste habe.
> Nachteil: Wenn der Rechner zu "schwach" ist, treten bei hoher
> Belastung Verzögerungen auf, da immer nur eine Message nach der
> anderen bearbeitet werden kann. Mit diesem Nachteil kann ich jedoch
> leben, solange kein Paket "verloren geht".
Wenn Du das optimieren willst, dann erhoehst Du die Anzahl der Threads die die
Nachrichten bearbeiten auf Anzahl CPUs - 1, oder je nach Anzahl der Clients
(Auslastung des Threads der select macht) auch auf die Anzahl der CPUs
(konfigurierbar machen). Unter der Voraussetzung, dass Deine Bearbeitungs-
Threads primaer rechnen (CPU bound) bekommst Du so die auf dem jeweiligen
Rechner optimale Performance.
Gegen einen "zu schwachen" Rechner ist nie ein Kraut gewachsen, das ist ein
Design-Fehler (zumindest dann, wenn es staendig auftritt).
Ein weiteres, von Dir offenbar nicht bedachtes Problem ist das Blockieren der
Bearbeitungs-Threads beim Senden, wenn mehr Daten verarbeitet werden, als
ueber's Netz rausgehen. Ich habe vor kurzem so was aehnliches gemacht, wie
das, was Du hier beschreibst (primaeres Target OS AIX), und da erzeuge ich,
falls der Bearbeitungsthread beim Senden blockieren will dynamisch einen
neuen, der nur fuer das Absenden fuer diesen einen Client zustaendig ist. Der
Performance-Verlust ist vernachlaessigbar, wenn diese Situation selten
auftritt. Tritt sie haeufiger auf, dann ist das Gesamtsystem sowieso falsch
designed. Ausgehende Nachrichten, die versendet werden sollen, waehrend dieser
Thread lebt, muessen in einem Zwischenpuffer gespeichert werden, damit nichts
verloren geht.
Das Ganze erfordert zwar etwas Sorgfalt im Detail, ist aber im Prinzip nicht
sonderlich schwierig. Wenn Du Beispielcode brauchst (C++) kannst Du Dich gerne
melden.
Gruss
Uz
--
Ullrich von Bassewitz u...@musoftware.de
Das habe ich nicht getan.
Ich stelle überhaupt echt wenige Leute als Anfänger hin. Ich zeige
höchstens auf Äußerungen, die diesen Schluß nahelegen.
> * Er soll auf "beliebig viele" UDP-Sockets lauschen können
> => Einsatz von select() oder poll() -- was sonst.
> * Jede auf einem Socket empfangene Message wird bearbeitet und eine
> Antwortmessage wird generiert, welche dann auf dem gleichen Socket
> zurückgesendet wird.
> * Die Bearbeitung von den empfangenen Messages und das Generieren der
> Antwortmessage braucht jedoch rel. viel CPU-Zeit, da ASN.1 De- und
> Encodierungen und diverse Crypto-Verfahren (Ver- und Entschlüsselung,
> Hashwerte, etc.) im Spiel sind.
> Möglichkeit 1:
> Man macht alles in einem Prozess -- ohne Threads.
> Vorteil: Einfach, da keine interne Synchronisation notwendig.
> Nachteil: Bei starker (burstartiger) Belastung kann es passieren,
> daß ich z.B. von einem Client mehr Messages empfange, als der Prozess
> abarbeiten kann. D.h., die Empfangs-Queue vom TCP/IP Stack wird
> irgendwann voll sein und ich verliere Pakete.
Wenn dein UDP-Protokoll nicht mit verlorenen Paketen klar kommt, ist
es natürlich eh total für die Tonne und wir müssen hier nicht weiter
Zeit verschwenden.
> Möglichkeit 2:
> Ich mache zwei Threads. Einer macht den select() und schreibt alle
> empfangenen Messages in eine Event-Liste, ohne die oben genannten
> Bearbeitungsschritte durchzuführen. Der zweite Thread arbeitet diese
> Liste Eintrag für Eintrag ab und sendet die Antwortpakete zurück zum
> Client.
> Vorteil: Die Empfangs-Queue vom TCP/IP Stack kann "schneller"
> ausgelesen werden, wodurch ich bei höherer Last weniger bzw. gar
> keine Paketverluste habe.
Der Kernel limitiert die Queue, um kein DoS-Probleme zu haben.
Entweder du limitierst deine Queue ebenfalls oder du hast ein DoS-Problem.
Wenn du nicht weißt, was das ist, solltest du keine Server programmieren.
> Nachteil: Wenn der Rechner zu "schwach" ist, treten bei hoher
> Belastung Verzögerungen auf, da immer nur eine Message nach der
> anderen bearbeitet werden kann. Mit diesem Nachteil kann ich jedoch
> leben, solange kein Paket "verloren geht".
Bei UDP können per Definition Pakete verloren gehen.
Die eigentliche Frage ist doch, ob dein Server die Anfragen in Echtzeit
wird beantworten können. Wenn nein, dann kauf mehr Hardware. Wenn
doch, dann setze den Puffer ausreichend hoch. Wenn das nicht reicht,
kann man das auch mit pipe() und fork() entkoppeln. Threads sind in
jedem Fall absolut überflüssig und schaden sogar, weil sie den libc-Code
komplexer und größer machen.
> Jedoch soll es unter WinNT auch funktionieren, was bedeutet, daß man
> viele der netten Unix-Spielereien (z.B. fork(), socketpair(), etc.)
> nicht verwenden kann. :-(
Unter Windows kann man nicht sauber programmieren, also unterstütze ich
auch keine Autoren dabei, ihre Software dort hinzuportieren.
Felix
Man kann zwischen UDP-Paketen, die auf dem Weg zum Server
verlorengehen und Paketen, die auf Grund eines zu kleinen
receive buffers verlorengehen, unterscheiden.
Gegen ersteres kann man serverseitig gar nichts machen, egal wie
super der Server implementiert ist.
Zweiteres kann man "verhindern", jedoch öffnet man sich,
wie Du weiter unten angesprochen hast, eine DoS-Möglichkeit.
> Der Kernel limitiert die Queue, um kein DoS-Probleme zu haben.
> Entweder du limitierst deine Queue ebenfalls oder du hast ein
> DoS-Problem.
ACK.
> Wenn du nicht weißt, was das ist, solltest du keine
> Server programmieren.
Ich bin momentan sowieso noch in der Planungsphase und habe daher
noch keine einzige Zeile Code geschrieben. D.h., ich habe noch
genug Zeit, Deine (berechtigten) Einwände zu berücksichtigen.
> Bei UDP können per Definition Pakete verloren gehen.
Siehe oben.
Wenn ein Router ein Packerl wegschmeißt, kann ich so oder so nix
dagegen machen. Mir ist es allerdings wichtiger, daß kein
"empfangenes" Paket verlorengeht.
> Die eigentliche Frage ist doch, ob dein Server die Anfragen in
> Echtzeit wird beantworten können.
Von Echtzeit war nie die Rede. Die Messages werden in der Reihenfolge,
wie sie eintreffen, abgearbeitet. Und falls eine Message erst eine
Sekunde nach dem Eintreffen abgearbeitet werden sollte, dann kann ich
damit leben.
> Wenn nein, dann kauf mehr Hardware.
Eh klar, daß die verfügbare Hardware die Anzahl der Messages, die
in einem gegebenen Zeitintervall abgearbeitet werden können,
begrenzt.
> Unter Windows kann man nicht sauber programmieren,
Das brauchst Du mir nicht zu sagen. Das weiß ich auch selbst. :-)
Ich kann allerdings nicht einfach hergehen, und die Menge der
geforderten Zielplattformen nach eigenem Ermessen reduzieren...
Bernhard, auch du willst dich _vor_ dem Benutzen eines Fachbegriffes
("Echtzeit") über seine Bedeutung informieren.
Echtzeit heißt nicht "schnell" oder "ohne Latenz".
> > Unter Windows kann man nicht sauber programmieren,
> Das brauchst Du mir nicht zu sagen. Das weiß ich auch selbst. :-)
> Ich kann allerdings nicht einfach hergehen, und die Menge der
> geforderten Zielplattformen nach eigenem Ermessen reduzieren...
Doch, natürlich.
Du programmierst POSIX und wenn das Windoze POSIX-Layer das nicht kann,
dann ist das halt Pech. Fertig. Sie werben damit, daß NT POSIX kann,
und daß 2000 NT-Technologie ist.
Felix
>> Ich weiß was select() und poll() ist. Ich bin ja schließlich kein
>> Anfänger, so wie Du mich hinzustellen versuchst...
> Das ist FvL, seine Kommentare kannst Du getrost ignorieren. Sie enthalten
> selten echte Information.
Sie regen aber zum Nachdenken und -lesen an, und das ist mindestens
genauso wichtig. Vorgekaute Informationen kann man auswendig lernen, man
lernt dabei aber nichts.
> Wenn Du das optimieren willst, dann erhoehst Du die Anzahl der Threads die die
> Nachrichten bearbeiten auf Anzahl CPUs - 1, oder je nach Anzahl der Clients
> (Auslastung des Threads der select macht) auch auf die Anzahl der CPUs
> (konfigurierbar machen).
Nein, man macht das überhaupt nicht mit Threads, wenn man es sauber
machen will. Der saubere Weg ist, das ganze über separate Prozesse zu
machen, die von einem zentralen Prozess, der die Pakete vom Netzwerk
entgegennimmt, mit Arbeit versorgt werden. Zur Kommunikation zwischen
den Prozessen bietet Unix genügend Möglichkeiten, z.B. Pipes, lokale
Sockets, Shared Memory etc. (im konkreten Fall würde ich wohl eine in
einem Shared Memory-Bereich liegende Queue nehmen, in die die Pakete vom
"Receiver" eingestellt und von den "Workern" entnommen werden, plus eine
Semaphore, um den Zugriff auf die Queue zu serialisieren).
Der Verzicht auf Threads erspart einem dabei einen ganzen Sack voller
Probleme (Stichworte wie Reentranz, Stabilität usw.), und erhältst
gleichzeitig neue Möglichkeiten, wie z.B. die einzelnen Teile unabhängig
voneinander testen zu können.
> Ein weiteres, von Dir offenbar nicht bedachtes Problem ist das Blockieren der
> Bearbeitungs-Threads beim Senden, wenn mehr Daten verarbeitet werden, als
> ueber's Netz rausgehen.
Dann muss die Empfangsqueue gross genug sein, um die im realen Betrieb
auftretenden Peaks abfangen zu können, bis wieder ein "Worker" frei ist.
Diese Situation tritt ja nur ein, wenn die Abarbeitung schneller ist als
das Netzwerk, also macht es hier keinen Unterschied, ob "auf Halde"
abgearbeitet wird oder die Pakete in der Empfangsqueue gesammelt werden.
BTW, bei einem TCP-basierten Server sähe das anders aus, da hier ein
Client die Möglichkeit hat, das Senden künstlich zu verlangsamen (auch
ohne böse Absichten) und so eine DoS-Situation herbeizuführen.
> Ich habe vor kurzem so was aehnliches gemacht, wie
> das, was Du hier beschreibst (primaeres Target OS AIX), und da erzeuge ich,
> falls der Bearbeitungsthread beim Senden blockieren will dynamisch einen
> neuen, der nur fuer das Absenden fuer diesen einen Client zustaendig ist.
Damit hast du den Stau von der Empfangsseite auf die Sendeseite verlegt.
Was hast du dabei gewonnen (ausser zusätzlicher Komplexität)?
Andreas
--
Andreas Ferber - dev/consulting GmbH - Bielefeld, FRG
---------------------------------------------------------
+49 521 1365800 - a...@devcon.net - www.devcon.net
> entwickelt. Jedoch soll es unter WinNT auch funktionieren, was
> bedeutet, daß man viele der netten Unix-Spielereien (z.B. fork(),
> socketpair(), etc.) nicht verwenden kann. :-(
Doch, mit Cygwin (http://cygwin.com/).
> Nein, man macht das ueberhaupt nicht mit Threads, wenn man es sauber
> machen will. Der saubere Weg ist, das ganze ueber separate Prozesse zu
> machen, die von einem zentralen Prozess, der die Pakete vom Netzwerk
> entgegennimmt, mit Arbeit versorgt werden.
Das ist solange richtig, bis die "POSIX"-Umgebung von Windows
genutzt werden soll. Ab dann sind Threads teilweise guenstiger.
Dein Einwand von unten (Sack voller Aerger) ist dann aber immer
noch berechtigt und sicher ein Grund fuer diverse Instabilitaeten
und Megaprogramme.
> Zur Kommunikation zwischen
> den Prozessen bietet Unix genuegend Moeglichkeiten, z.B. Pipes, lokale
> Sockets, Shared Memory etc. (im konkreten Fall wuerde ich wohl eine in
> einem Shared Memory-Bereich liegende Queue nehmen, in die die Pakete vom
> "Receiver" eingestellt und von den "Workern" entnommen werden, plus eine
> Semaphore, um den Zugriff auf die Queue zu serialisieren).
Wenn du das mit Shared Memory und einer Semaphore machst, kannst
du auch gleich Threads nehmen.
Pipes haben den Vorteil, Daten zu transferieren UND gleichzeitig
als Semaphore zu fungieren. Bei Paketen im Bereich bis PIPE_BUF
Bytes wuerde ich eine Pipe auf jeden Fall vorziehen. Und das in
jede der Richtungen.
Ausserdem kannst du auch gleichzeitig warten oder in den Workern
mehrere Deskriptoren bedienen (warum auch immer).
Diverses Fenstermanager implementieren z.B. so erfolgreich ihre
Module. Wie das geht kannst du dir dort ansehen.
Die Worker belegst du dann z.B. im Round-Robin-Verfahren mit
Arbeit und wenn nach einer Runde alle input-pipes EAGAIN liefern,
dann arbeitest du in eine Speicherqueue hinein und arbeitest die
dann ab (oder auch nicht, wenn du z.B. softe Echtzeit-Anforderungen
hast).
Ohne diese Queue wuerde sich allerdings ein natuerliches
Load-Management ergeben ;-)
Pipes haben weiterhin den Vorteil, dass sie auf jedem
anstaendigen Unix recht fix sind, da sowas halt sehr haeufig zum
Einsatz kommt.
> Der Verzicht auf Threads erspart einem dabei einen ganzen Sack voller
> Probleme (Stichworte wie Reentranz, Stabilitaet usw.), und erhaeltst
> gleichzeitig neue Moeglichkeiten, wie z.B. die einzelnen Teile unabhaengig
> voneinander testen zu koennen.
Man kann mittels pipes dann auch stdin und stdout gleich entsprechend
verschalten, um das dann wirklich als Blackbox zu testen.
Was auch geht: Den Spass ueber msg-queues realisieren. Damit
eruebrigt sich dann der Speicherpuffer fuer zu schnell erhaltene
Requests und die Semaphore spart man sich auch hier, da
msg-queues implizit synchronisieren.
Der "Shared Memory + Semaphore"-Ansatz macht meiner Meinung nach
nur bei groszen "Paketen" (fuer die Worker) Sinn. Bisher hab ich
das auch nur in solchen Faellen gesehen (z.B. bei mpg123).
Das wiederum kann man durch Zusammenfassen von kleinen zu groszen
Paketen erreichen. Man schaltet dann eben erst nach 100K
empfangenden Daten oder einer Sekunde um, je nachdem was frueher
passiert.
Die Semaphoren des Systems zu benutzen ist auf jeden Fall extrem
langsam. So langsam, dass ein zusaetzlices rumkopieren da meist
nicht auffaellt. Meist bieten die Thread-Pakete da irgendwas
schnelleres, wenn es innerhalb des selben Adreszraumes liegt.
Deswegen auf jeden Fall erstmal etwas anderes probieren, bevor
man die SysV-Semaphoren verwendet in seinen Prozessen. Sonst wird
das Thread-Argument leider ziehen.
Weiteres Problem: Es ist NICHTS ueber die Daten bekannt, die nun
im UDP-Paket liegen. Ich gehe deshalb mal davon aus, dass es
wirklich unabhaengige Datagramme sind, deren Reihenfolge des
Eintreffens ebenfalls egal ist. Wenn der OP mal naeher darauf
eingehen wuerde, koennte man ihm auch besser helfen.
Ok, das wars erstmal ;-)
Grusz
Ingo
--
In der Wunschphantasie vieler Mann-Typen [ist die Frau] unsigned und
operatorvertraeglich. --- Dietz Proepper in dasr
Jein (mit Tendenz zum Nein). Bei Shared Memory hast du gegenüber Threads
ja nur einen ganz bestimmten Überschneidungspunkt zwischen den
Prozessen, und nur beim Zugriff darauf musst du besondere Vorsicht
walten lassen.
> Pipes haben den Vorteil, Daten zu transferieren UND gleichzeitig
> als Semaphore zu fungieren. Bei Paketen im Bereich bis PIPE_BUF
> Bytes wuerde ich eine Pipe auf jeden Fall vorziehen. Und das in
> jede der Richtungen.
>
> Ausserdem kannst du auch gleichzeitig warten oder in den Workern
> mehrere Deskriptoren bedienen (warum auch immer).
>
> Diverses Fenstermanager implementieren z.B. so erfolgreich ihre
> Module. Wie das geht kannst du dir dort ansehen.
>
> Die Worker belegst du dann z.B. im Round-Robin-Verfahren mit
> Arbeit und wenn nach einer Runde alle input-pipes EAGAIN liefern,
> dann arbeitest du in eine Speicherqueue hinein und arbeitest die
> dann ab (oder auch nicht, wenn du z.B. softe Echtzeit-Anforderungen
> hast).
Dann musst du aber wieder mit select()/poll() arbeiten (oder du
verzichtest auf die Speicherqueue und verwirfst Pakete, wenn alle Pipes
EAGAIN liefern). Ich hatte bei meiner Shared Memory/Semaphoren-Idee
eigentlich im Hinterkopf, daß man dann einfach mit einem blockierenden
Socket arbeiten kann.
Ausserdem ist dieser Ansatz ungünstig, wenn die Bearbeitungszeiten der
einzelnen Pakete stark schwanken. Beim Ansatz mit einer gemeinsamen
Queue (oder einer Message Queue, s. u.) kann sich ein Worker selbst ein
neues Paket holen, wenn er mit seinem fertig ist, während beim
Pipe-Ansatz die Verteilung vom Receiver abhängt.
> Ohne diese Queue wuerde sich allerdings ein natuerliches
> Load-Management ergeben ;-)
Ack. Aber nach Aussage des OP ist die Latenz ja egal, so daß ich
derartige Überlegungen erstmal ausgeklammert habe.
>> Der Verzicht auf Threads erspart einem dabei einen ganzen Sack voller
>> Probleme (Stichworte wie Reentranz, Stabilitaet usw.), und erhaeltst
>> gleichzeitig neue Moeglichkeiten, wie z.B. die einzelnen Teile unabhaengig
>> voneinander testen zu koennen.
> Man kann mittels pipes dann auch stdin und stdout gleich entsprechend
> verschalten, um das dann wirklich als Blackbox zu testen.
Du musst sowieso ein Programm schreiben, das Testpakete generiert, ob
diese dann nach stdout geschrieben werden oder im Shared Memory abgelegt
werden ist IMHO nebensächlich ;-)
> Was auch geht: Den Spass ueber msg-queues realisieren. Damit
> eruebrigt sich dann der Speicherpuffer fuer zu schnell erhaltene
> Requests und die Semaphore spart man sich auch hier, da
> msg-queues implizit synchronisieren.
Ack, an Message Queues hatte ich nicht gedacht. Allerdings kann ein
UDP-Paket grösser als MSGMAX sein.
Denkbar wäre noch ein kombinierter Einsatz von Shared Memory und Message
Queue: der Receiver legt ein Paket im Shared Memory ab und versendet
über die Message Queue eine Message mit der Slotnummer des neuen
Paketes. Um einen Slot wieder freizugeben, muss der Worker dann ein mit
dem Slot assoziiertes Flag im Shared Memory auf Null setzen (das geht
atomar), belegt werden Slots nur vom Receiver.
Da die Slotnummer wenig Platz braucht und eine konstante Grösse hat,
könnte man statt der Message Queue auch noch eine Pipe verwenden: die
Worker lesen alle blockierend aus _derselben_ Pipe, in die vom Receiver
geschrieben wird. Die Slotnummer ist klein genug, daß sie atomar aus der
Pipe gelesen werden kann. Wenn mehrere Worker gleichzeitig im read()
blockieren, dann wird der Kernel genau einen Worker aufwecken, wenn der
Receiver eine Slotnummer in die Pipe schreibt. Soweit genug Slots im
Shared Memory zur Verfügung stehen, hat man durch den Pipe-Buffer in
diesem Fall ein ziemlich grosses Backlog zur Verfügung (aber Vorsicht,
die Latenzen können dabei enorm ansteigen).
> Der "Shared Memory + Semaphore"-Ansatz macht meiner Meinung nach
> nur bei groszen "Paketen" (fuer die Worker) Sinn. Bisher hab ich
> das auch nur in solchen Faellen gesehen (z.B. bei mpg123).
Klar, je grösser die Arbeitseinheiten, desto geringer fällt der Overhead
prozentual aus.
> Das wiederum kann man durch Zusammenfassen von kleinen zu groszen
> Paketen erreichen. Man schaltet dann eben erst nach 100K
> empfangenden Daten oder einer Sekunde um, je nachdem was frueher
> passiert.
Das bringt bei schwankenden Bearbeitungszeiten wieder die gleichen
Probleme wie bei den Pipes oben.
> Die Semaphoren des Systems zu benutzen ist auf jeden Fall extrem
> langsam. So langsam, dass ein zusaetzlices rumkopieren da meist
> nicht auffaellt. Meist bieten die Thread-Pakete da irgendwas
> schnelleres, wenn es innerhalb des selben Adreszraumes liegt.
Da wird es dann aber schnell architekturabhängig. Bei x86 ist es ja z.B.
kein Problem, über Shared Memory eine Semaphore komplett im Userspace zu
implementieren (der Code ist identisch zu den vom Kernel intern
benutzten Semaphoren, da die benötigten Befehle alle nicht privilegiert
sind), während das auf anderen (RISC-)Architekturen nicht so ohne
weiteres geht, bzw. auf andere, sehr zeitraubende Mechanismen
ausgewichen werden muss.
> Weiteres Problem: Es ist NICHTS ueber die Daten bekannt, die nun
> im UDP-Paket liegen. Ich gehe deshalb mal davon aus, dass es
> wirklich unabhaengige Datagramme sind, deren Reihenfolge des
> Eintreffens ebenfalls egal ist. Wenn der OP mal naeher darauf
> eingehen wuerde, koennte man ihm auch besser helfen.
Insbesondere wäre es auch interessant zu wissen, wie gross die
UDP-Datagramme werden (s.o.) ;-)
Wenn du mal einen diskreten Blick auf den Newsgroupnamen wirfst, wirst
du feststellen, daß wir hier _nicht_ über Windoze und die diversen
assoziierten Hirnschäden sprechen, sondern über Unix.
Ich weigere mich daher, mir hier Windoze-Kompatibilitäts-Bullshit
durchlesen zu müssen.
Multithreading ist und bleibt eine schlechte Idee, und es gibt außer der
miserablen fork()-Performance von Solaris keinen Grund, sich (auch nur
kurzzeitig) mit Threads auseinanderzusetzen.
Felix
Doch, um zu der Erkenntnis zu kommen, daß Threads nichts taugen, muss
man sich zunächst mit ihnen auseinandersetzen ;-)
SCNR, Andreas
Selbst wenn ich die staendigen Rumpoebeleien von FvL mal ignoriere (was ich in
Wirklichkeit natuerlich nicht tue, ich finde es peinlich und beschaemend),
vielleicht kannst Du mir als Beispiel ja mal sagen, was mich an
<3b84...@fefe.de> zum Nachdenken und Nachlesen anregen soll.
Oder nein - sag mir's lieber nicht. Ich will diese Diskussion eigentlich
ueberhaupt nicht fuehren. Ich rede gerne mit Dir ueber fachliche Dinge (s.u),
ich rede aber nicht mit/ueber FvL (letzte Anmerkung zu diesem Thema - grosses
Ehrenwort).
> Der Verzicht auf Threads erspart einem dabei einen ganzen Sack voller
> Probleme (Stichworte wie Reentranz, Stabilität usw.), und erhältst
> gleichzeitig neue Möglichkeiten, wie z.B. die einzelnen Teile unabhängig
> voneinander testen zu können.
Reentranzprobleme kannst Du auch mit separaten Prozessen bekommen, und die
Stabilitaet haengt nicht von der Tatsache ab, das Threads verwendet werden,
sondern davon, wieviele Fehler Du reinmachst. Soll heissen: Threads sind nicht
per se fehlertraechtiger als separate Prozesse. Ich schreibe seit mehr als 10
Jahren Programme, die Threads verwenden und bin immer gut damit gefahren. Ob
Du mehr oder weniger Probleme damit hast haengt natuerlich auch etwas von
Deiner Erfahrung und von Deinem Verstaendnis fuer die Materie ab.
> Dann muss die Empfangsqueue gross genug sein, um die im realen Betrieb
> auftretenden Peaks abfangen zu können, bis wieder ein "Worker" frei ist.
Natuerlich. Das ist der Sinn dieser Queue.
> Diese Situation tritt ja nur ein, wenn die Abarbeitung schneller ist als
> das Netzwerk, also macht es hier keinen Unterschied, ob "auf Halde"
> abgearbeitet wird oder die Pakete in der Empfangsqueue gesammelt werden.
Das ist dann nicht richtig, wenn andere Prozesse auf dem Rechner laufen, die
auch CPU Last erzeugen, oder wenn das fuer andere Teile Deines Programms so
ist. Brauchen andere, asynchrone Teile Deines Programms auch in regelmaessigen
Abstaenden CPU Zeit, dann sammeln sich die Daten erst in der Empfangsqueue,
und sobald wieder genug CPUs frei werden, in der Sendequeue (vorausgesetzt,
die Abarbeitung ist schneller als das Netz). Laesst Du die Worker-Threads
blockieren, dann geht CPU Zeit verloren, d.h. es kann sein, dass CPUs Idle
laufen, obwohl eigentlich Daten fuer zur Bearbeitung bereit stehen. Damit
verschenkst Du Hardware-Resourcen, die evtl. spaeter nicht mehr da sind, und
verzoegerst damit die Abarbeitung als Ganzes. Deine Behauptung waere korrekt
unter einer Annahme, dass nur die genannten Threads/Prozesse existieren, und
dass die zur Verfuegung stehende CPU Leistung deshalb konstant ist. In der
Praxis (zumindest wie ich sie kenne) ist das eine eher unwahrscheinliche
Annahme, ein Programm, das diesen Faktor nicht beruecksichtigt also schlecht
designed. Deshalb habe ich es anders geloest.
> Damit hast du den Stau von der Empfangsseite auf die Sendeseite verlegt.
> Was hast du dabei gewonnen (ausser zusätzlicher Komplexität)?
Ich habe die Performance des Servers insofern verbessert, als dass zur
Verfuegung stehende CPU Zeit, wenn sie benoetigt wird, auch wirklich genutzt
wird. Der Code ist zugegebenermassen etwas komplexer, das liegt aber alles
noch im Rahmen, sprich: Lief bei mir praktisch auf Anhieb fehlerfrei (bis
jetzt - das muss man bei Software immer dazusagen). Du hast allerdings
insofern recht, dass die zusaetzliche Komplexitaet bei der Verwendung von
separaten Prozessen (anstelle von Threads) noch mal ein ganzes Stueck groesser
waere. Wenn ich separate Prozesse verwendet haette, dann haette ich mir das
auch dreimal ueberlegt.
Und: Die von mir beschriebene Vorgehensweise ist, wie Du bereits richtig
bemerkt hast, bei TCP Verbindungen zwingend notwendig. Wenn Du Deinen Code
also wiederverwendbar halten willst (und die besprochene Funktionalitaet
laesst sich sehr gut in eine entsprechende Library packen), dann ist die von
mir angesprochene Erweiterung praktisch Pflicht.
Oh, das wurmt dich jetzt, ja?
Du hast eine falsche Aussage gepostet und ich habe öffentlich
widersprochen. Oh Mann, das ist ja wirklich ein Affrond!
Ullrich, wenn es bei dir keinen zur Resonanz anregbaren Schwingkörper im
Kopf gibt, dann ist das bedauerlich, aber nicht meine Schuld.
Andere Leute an deiner Stelle hätten an der Stelle vielleicht gemerkt,
daß man Aussagen prüfen sollte, bevor man sie öffentlich trifft.
> Oder nein - sag mir's lieber nicht. Ich will diese Diskussion eigentlich
> ueberhaupt nicht fuehren.
Klar doch. Was man deutlich daran erkennt, daß du sie bei jeder
Gelegenheit von neuem lostrittst. Sicher doch, Ullrich.
> Ich rede gerne mit Dir ueber fachliche Dinge (s.u), ich rede aber
> nicht mit/ueber FvL (letzte Anmerkung zu diesem Thema - grosses
> Ehrenwort).
Yeah, right.
> Reentranzprobleme kannst Du auch mit separaten Prozessen bekommen, und die
> Stabilitaet haengt nicht von der Tatsache ab, das Threads verwendet werden,
> sondern davon, wieviele Fehler Du reinmachst. Soll heissen: Threads sind nicht
> per se fehlertraechtiger als separate Prozesse. Ich schreibe seit mehr als 10
> Jahren Programme, die Threads verwenden und bin immer gut damit gefahren. Ob
> Du mehr oder weniger Probleme damit hast haengt natuerlich auch etwas von
> Deiner Erfahrung und von Deinem Verstaendnis fuer die Materie ab.
Ullrich, du kannst dich hier drehen und wenden wie du möchtest, und du
kannst uns auch gerne die Argumente im Mund umzudrehen versuchen, aber
es bleibt Fakt, daß Threads überflüssig sind, Code komplizierter und
schwerer wartbar machen. Dein "Argument", daß du das seit Jahren ohne
Probleme betreibst, besagt nur eines, nämlich daß man mit Threads
programmieren kann. Das hat hier niemand bestritten. Ich kann auch mit
Threads programmieren. Ich kann sogar ziemlich gut mit Threads
programmieren. Diese Erfahrung erlaubt mir, den Schluß zu ziehen, daß
ich _nicht_ mit Threads programmieren sollte.
Man kann auch den ganzen Tag mit einem Spiegelei auf der Stirn
herumlaufen. Und wenn du das seit Jahren machst, dann ist das schön für
dich, aber es bleibt trotzdem nicht nur nicht sinnvoll, sondern auch
kontraproduktiv.
> Das ist dann nicht richtig, wenn andere Prozesse auf dem Rechner laufen, die
> auch CPU Last erzeugen, oder wenn das fuer andere Teile Deines Programms so
> ist. Brauchen andere, asynchrone Teile Deines Programms auch in regelmaessigen
> Abstaenden CPU Zeit, dann sammeln sich die Daten erst in der Empfangsqueue,
> und sobald wieder genug CPUs frei werden, in der Sendequeue (vorausgesetzt,
> die Abarbeitung ist schneller als das Netz). Laesst Du die Worker-Threads
> blockieren, dann geht CPU Zeit verloren, d.h. es kann sein, dass CPUs Idle
> laufen, obwohl eigentlich Daten fuer zur Bearbeitung bereit stehen.
Deine Aussage ist nicht nachvollziehbar. Worker-Threads werden _immer_
blockieren, wenn die Queue leer ist, und sonst nicht. Nichts im Thread
vorher deutet darauf hin, daß jemand Worker-Threads häufiger blockieren
lassen möchte.
Ob du die Arbeit in einem anderen Thread oder einem anderen Prozeß
machst, spielt fast keine Rolle. Bis ein Thread/Prozeß segfaulted.
Wenn ein Apache-Plugin unter NT abraucht, ist Apache weg. Unter Linux
ist der Prozeß weg, Apache läuft insgesamt weiter. Wenn bei deinen
Applikationen also Fehlertoleranz nicht gebraucht, dann gilt das noch
lange nicht für alle anderen Leuten.
Prozesse statt Threads sorgen außerdem dafür, daß man eine beobachtbare
minimale Schnittstelle hat (die man sogar mit strace beobachten kann)
und Fehlverhalten in einem Thread nicht die Datenstrukturen eines
anderen Threads korrumpieren kann. Klar kann man argumentieren, daß in
korrekten Programmen keine Fehler auftauchen. In der Praxis ist es
trotzdem sinnvoll, ein Betriebssystem mit Speicherschutz zu benutzen.
Und Threads zu vermeiden.
> Und: Die von mir beschriebene Vorgehensweise ist,
Du hast da keine Vorgehensweise beschrieben, sondern irgendwelche
Nullaussagen über deinen Code getroffen, wie "ich habe die Performance
verbessert, indem ich CPU-Zeit voll ausnutze".
Überhaupt zeugt dein Posting nicht gerade von hoher Kompetenz als
Programmierer, denn dann könntest du klar und sauber kommunizieren, was
du sagen willst, und müßtest dich hier nicht in Allgemeinplätzen
ergehen.
Felix
--
1) Thread switching is faster than process switching by a little bit on Linux
2) Process switching on linux is faster than thread switching on NT
3) Almost anything is faster than process switching on NT
Aber nur dann, wenn ich sie mir selbst mache, sie sind nicht implizit
immer vorhanden.
> und die
> Stabilitaet haengt nicht von der Tatsache ab, das Threads verwendet werden,
> sondern davon, wieviele Fehler Du reinmachst. Soll heissen: Threads sind nicht
> per se fehlertraechtiger als separate Prozesse.
Doch. Moderne Betriebssysteme bieten Mechanismen, um Prozesse
voreinander zu schützen. Warum sollte ich diese Mechanismen mutwillig
_komplett_ ausschalten? Macht ein Thread Unfug, ist der komplette Server
weg, während bei separaten Prozessen ein einzelner abraucht und die
anderen unbeeindruckt weiterarbeiten. Der abgerauchte Prozess kann
leicht ersetzt werden.
> Ich schreibe seit mehr als 10
> Jahren Programme, die Threads verwenden und bin immer gut damit gefahren. Ob
> Du mehr oder weniger Probleme damit hast haengt natuerlich auch etwas von
> Deiner Erfahrung und von Deinem Verstaendnis fuer die Materie ab.
Ich kann auch mit Threads umgehen, und habe auch keine Probleme bei der
Umsetzung. Aber _gerade_ weil ich mich mit Threads und ihren
Implikationen auskenne, will ich sie nicht benutzen, wann immer es sich
vermeiden lässt. Threads sind eine Krücke, die erfunden wurde, um
Performanceprobleme mit Prozessen (fork() und Kontextwechsel) oder
mangelnde/unperformante IPC-Mechanismen zu kaschieren. Bei modernen
Betriebssystemen ist in Punkto Performance zwischen Threads und
Prozessen kaum noch ein Unterschied (bzw. es wird z.B. im Fall von Linux
nicht einmal mehr zwischen Prozessen und Threads unterschieden), und
ausgefeilte IPC-Mechanismen existieren auch, ergo sind Threads ein
Kropf, den es auszumerzen gilt.
> Das ist dann nicht richtig, wenn andere Prozesse auf dem Rechner laufen, die
> auch CPU Last erzeugen, oder wenn das fuer andere Teile Deines Programms so
> ist. Brauchen andere, asynchrone Teile Deines Programms auch in regelmaessigen
> Abstaenden CPU Zeit, dann sammeln sich die Daten erst in der Empfangsqueue,
> und sobald wieder genug CPUs frei werden, in der Sendequeue (vorausgesetzt,
> die Abarbeitung ist schneller als das Netz). Laesst Du die Worker-Threads
> blockieren, dann geht CPU Zeit verloren, d.h. es kann sein, dass CPUs Idle
> laufen, obwohl eigentlich Daten fuer zur Bearbeitung bereit stehen. Damit
> verschenkst Du Hardware-Resourcen, die evtl. spaeter nicht mehr da sind, und
> verzoegerst damit die Abarbeitung als Ganzes.
Wenn dein System zusammenbricht, bloss weil einmal alle darauf laufenden
Dienste für längere Zeit Vollast fahren, dann machst du was flasch.
> Deine Behauptung waere korrekt
> unter einer Annahme, dass nur die genannten Threads/Prozesse existieren, und
> dass die zur Verfuegung stehende CPU Leistung deshalb konstant ist.
Entweder dein System hat die Leistungsreserven, um auch bei Hochlast
nicht zusammenzubrechen, oder die Dienste darauf scheinen wohl doch
nicht so wichtig zu sein.
> In der
> Praxis (zumindest wie ich sie kenne) ist das eine eher unwahrscheinliche
> Annahme, ein Programm, das diesen Faktor nicht beruecksichtigt also schlecht
> designed.
Ja, der Designfehler ist, daß deinem Dienst nicht genügend Ressourcen
zur Verfügung gestellt werden. Wenn genügend Ressourcen da wären, dann
wären die auch "irgendwann später" nicht auf einmal weg.
>> Damit hast du den Stau von der Empfangsseite auf die Sendeseite verlegt.
>> Was hast du dabei gewonnen (ausser zusätzlicher Komplexität)?
> Ich habe die Performance des Servers insofern verbessert, als dass zur
> Verfuegung stehende CPU Zeit, wenn sie benoetigt wird, auch wirklich genutzt
> wird. Der Code ist zugegebenermassen etwas komplexer, das liegt aber alles
> noch im Rahmen, sprich: Lief bei mir praktisch auf Anhieb fehlerfrei (bis
> jetzt - das muss man bei Software immer dazusagen). Du hast allerdings
> insofern recht, dass die zusaetzliche Komplexitaet bei der Verwendung von
> separaten Prozessen (anstelle von Threads) noch mal ein ganzes Stueck groesser
> waere. Wenn ich separate Prozesse verwendet haette, dann haette ich mir das
> auch dreimal ueberlegt.
Und ich hätte egal ob ich jetzt Threads oder Prozesse benutze garnicht
überlegt, sondern von vornherein keine eigene Output-Queue eingeführt,
da ich weiss, daß mein Betriebssystem zu meinem Socket auch eine Send
Queue verwaltet (deren Grösse ich zudem falls nötig auch noch anpassen
kann). Wenn diese volläuft, dann liegt eine Überlast-Situation vor und
ich muss wohl oder übel den Worker vorerst auf Eis legen, bis sich die
Wogen wieder ein wenig geglättet haben.
> Und: Die von mir beschriebene Vorgehensweise ist, wie Du bereits richtig
> bemerkt hast, bei TCP Verbindungen zwingend notwendig. Wenn Du Deinen Code
> also wiederverwendbar halten willst (und die besprochene Funktionalitaet
> laesst sich sehr gut in eine entsprechende Library packen), dann ist die von
> mir angesprochene Erweiterung praktisch Pflicht.
Routinen, die für einen UDP-Dienst geschrieben wurden, laufen nicht auf
einmal auch mit TCP. Natürlich kannst (und solltest) du die Routinen,
die die eigentlichen Daten verarbeiten (es war von ASN.1 und Crypto die
Rede) so allgemein wie möglich halten, die Netzwerkkommunikation wirst
du aber auf jeden Fall umschreiben müssen. Wenn du diese beiden Module
nicht sauber trennen kannst, dann machst du was flasch.
> Aber nur dann, wenn ich sie mir selbst mache, sie sind nicht implizit
> immer vorhanden.
Das ist bei Threads genauso:-)
> Doch. Moderne Betriebssysteme bieten Mechanismen, um Prozesse
> voreinander zu schützen. Warum sollte ich diese Mechanismen mutwillig
> _komplett_ ausschalten? Macht ein Thread Unfug, ist der komplette Server
> weg, während bei separaten Prozessen ein einzelner abraucht und die
> anderen unbeeindruckt weiterarbeiten. Der abgerauchte Prozess kann
> leicht ersetzt werden.
Wenn einer Deiner Prozesse abraucht steht das System genauso. Und ich wuerde
in beiden Faellen Vorkehrungen treffen, den oder die Prozesse wieder zu
starten - Threads oder nicht.
> Threads sind eine Krücke, die erfunden wurde, um
> Performanceprobleme mit Prozessen (fork() und Kontextwechsel) oder
> mangelnde/unperformante IPC-Mechanismen zu kaschieren.
Wenn ich Threads verwende, dann nicht aus Performance-Gruenden, sondern weil
sie die Kommunikation zwischen den verschiedenen Teilen des Programms leichter
machen, und deshalb das Design vereinfachen.
> Bei modernen
> Betriebssystemen ist in Punkto Performance zwischen Threads und
> Prozessen kaum noch ein Unterschied (bzw. es wird z.B. im Fall von Linux
> nicht einmal mehr zwischen Prozessen und Threads unterschieden), und
> ausgefeilte IPC-Mechanismen existieren auch, ergo sind Threads ein
> Kropf, den es auszumerzen gilt.
Du nennst einen angeblichen Vorteil von Threads, von dem ich nicht gesprochen
habe (und den ich auch nicht anfuehren wuerde) und argumentierst dann gegen
diesen angeblichen Vorteil. In Wirklichkeit liegt die Vorteile von Threads
aber ganz woanders:
* Threads erlauben eine einfachere Abstimmung zwischen den verschiedenen
Teilen des Programms.
* Sie sind beim heutigen Stand der Technik portabler. Ich habe es nicht
probiert, weil ich nicht dafuer bezahlt wurde, aber ich gehe davon aus,
dass sich mein Programm in etwa einem Tag auf Windows portieren laesst,
falls es mal notwendig sein sollte. Alle Spezialitaeten wie Threads,
Synchronisationsmechanismen sind gekappselt, und existieren auch in
einer Version fuer Windows, der Code selber enthaelt keine
betriebssystemspezifischen Routinen.
>> Das ist dann nicht richtig, wenn andere Prozesse auf dem Rechner laufen, die
[...]
> Wenn dein System zusammenbricht, bloss weil einmal alle darauf laufenden
> Dienste für längere Zeit Vollast fahren, dann machst du was flasch.
Warum gehst Du nicht einfach auf meinen Einwand ein? Ich habe Dir erklaert,
warum meine Massnahme Sinn macht. Ein Server mit dieser Aenderung laeuft auch
bei wechselnden Lasten auf dem Server performanter als einer ohne. Es geht
auch nicht darum, dass irgendein Dienst mal Vollast faehrt, oder dass das
System zusammenbricht. Funktionieren tut Deine und meine Variante. Wenn aber
der Admin mal einen Backup im Hintergrund macht oder sonstwas lauft, dann
nutzt meine Version die zur Verfuegung stehenden Resourcen moeglichst gut aus,
um trotzdem eine anstaendige Performance zusammenzubekommen, und Deine tut das
eben nicht.
Wenn ich mich recht erinnere, dann ist das einer der Unterschiede zwischen
gutem und nicht so gutem Design.
> Ja, der Designfehler ist, daß deinem Dienst nicht genügend Ressourcen
> zur Verfügung gestellt werden. Wenn genügend Ressourcen da wären, dann
> wären die auch "irgendwann später" nicht auf einmal weg.
Nachdem Dein erstes Posting durchaus auf solides technisches Verstaendnis
schliessen lies bin ich jetzt ehrlich gesagt etwas enttaeuscht. Denk mal nach!
Die Resourcen sind natuerlich da. Du beschaffst Hardware aber nie so, dass sie
zu jedem beliebigen Zeitpunkt jede gewuenschte Spitzenleistung bringen kann,
das waere voelliger Unsinn. Deshalb gibt es auch Betriebssysteme, die
Rechenleistung und Resourcen auf mehrere Prozesse verteilen koennen. Auf
Dauer, d.h. im Durchschnitt gesehen sind die notwendigen Resourcen naemlich
sehr wohl da, aber eben nicht in jedem beliebigen Moment. Um Daten dort zu
puffern, wo sie nicht direkt verarbeitet werden koennen, enthaelt das
Betriebssystem z.B. Empfangspuffer, das Programm selber nochmal eine
Empfangsqueue usw. Bei Deiner Variante kann es vorkommen, dass Daten im
Empfangspuffer stehen, die CPU frei ist - und trotzdem keine Daten verarbeitet
werden. Kommt spaeter eine Situation wo die Daten gebraucht werden, die CPU
aber kurzfristig nicht mehr frei ist - Pech gehabt. Meine Loesung behebt
diesen Missstand, mehr nicht.
> Und ich hätte egal ob ich jetzt Threads oder Prozesse benutze garnicht
> überlegt, sondern von vornherein keine eigene Output-Queue eingeführt,
> da ich weiss, daß mein Betriebssystem zu meinem Socket auch eine Send
> Queue verwaltet (deren Grösse ich zudem falls nötig auch noch anpassen
> kann). Wenn diese volläuft, dann liegt eine Überlast-Situation vor und
> ich muss wohl oder übel den Worker vorerst auf Eis legen, bis sich die
> Wogen wieder ein wenig geglättet haben.
Dieser Puffer kann nicht beliebig gross gemacht werden (ich habe 64K in
Erinnerung). Ich will mich auch nicht mit Dir streiten. Mein Programm
beruecksichtigt diese Situation von selber und es laeuft zuverlaessig. Wenn Du
fuer Deine Programme entscheidest, dass Dir der zusaetzliche Code zu komplex
oder fehlertraechtig ist, dann ist das Deine Entscheidung. Ich habe auch schon
Dinge abgelehnt, weil sie mir als unangemessen schienen (und durchaus auch mal
im Nachhinein festgestellt, dass meine Entscheidung falsch war).
Fuer den Fall, dass Du vielleicht doch mal in die Verlegenheit kommen solltest
habe ich nachgeschaut, wie gross der zusaetzliche Aufwand ist: Bei mir
betraegt er ganze 150 Zeilen Code (beim Schreiben einer Nachricht). Ich
schreibe sehr "luftigen" Code (manchmal auch Weichei-Code genannt:-), mit viel
leeren Zeilen und Kommentaren, Hardcore-Programmierer kriegen das bestimmt
deutlich unter 100 Zeilen. 150 Zeilen scheinen mir nicht so uebermaessig
komplex und unbeherrschbar zu sein, und ich kann mich in dem Programmteil auch
nicht an Fehler erinnern.
Wohlgemerkt: Die 150 Zeilen setzen eine sorgfaeltige Kapselung der Low Level
Bestandteile voraus (die hier aber sowieso vorhanden war). Wenn Du ohne
Kapselung mit pthread_irgendwas arbeitest, dann musst Du die Zeilenangabe
mindestens verdoppeln.
> Routinen, die für einen UDP-Dienst geschrieben wurden, laufen nicht auf
> einmal auch mit TCP. Natürlich kannst (und solltest) du die Routinen,
> die die eigentlichen Daten verarbeiten (es war von ASN.1 und Crypto die
> Rede) so allgemein wie möglich halten, die Netzwerkkommunikation wirst
> du aber auf jeden Fall umschreiben müssen. Wenn du diese beiden Module
> nicht sauber trennen kannst, dann machst du was flasch.
Das Empfangsteil und Sendeteil, d.h. der Teil des Programms, der mit den
Clients redet, die Clientliste verwaltet etc. (und nur um diesen ging es hier,
von der Verarbeitung hat niemand geredet) laesst sich sehr gut als separate
Bibliothek schreiben, und das habe ich bei mir auch so gemacht. Ob Du UDP oder
TCP machst bedingt nur sehr wenige Aenderungen beim Erzeugen und teilweise
beim Behandeln der Sockets. Diese lassen sich in meinem Fall (weil C++) leicht
durch virtuelle Methoden dem jeweiligen Netzwerksprotokoll anpassen.
> > Threads sind eine Krücke, die erfunden wurde, um
> > Performanceprobleme mit Prozessen (fork() und Kontextwechsel) oder
> > mangelnde/unperformante IPC-Mechanismen zu kaschieren.
>
> Wenn ich Threads verwende, dann nicht aus Performance-Gruenden, sondern weil
> sie die Kommunikation zwischen den verschiedenen Teilen des Programms leichter
> machen, und deshalb das Design vereinfachen.
Das wuerde ich nicht so sehen. Prozesse zwingen einen meistens die Interprozess
kommuniation ueber explizite Nachrichten abzuwickeln (jedenfalls wenn du shm
ignorierst), was dem Design meistens sehr gut tut, da es den Designer dazu
zwingt sich mehr Gedanken ueber die Kommunikation zwischen seinen Tasks zu
machen. Mit Threads und Shared memory ist es viel einfacher schnelle
Hackloesungen ohne viel Nachdenken einzubauen ("ich baue hier einfach mal
die Variable mit dem Lock rein -- wird schon funktionieren") und langfristig
faellt dann irgendwann alles zusammen. Wenn du fuer jede Interaktion explizit
eine Nachricht definierst und dir darueber Gedanken machen musst wo sich
die Prozesse jetzt treffen wird alles gleich viel sauberer.
> * Sie sind beim heutigen Stand der Technik portabler. Ich habe es nicht
> probiert, weil ich nicht dafuer bezahlt wurde, aber ich gehe davon aus,
> dass sich mein Programm in etwa einem Tag auf Windows portieren laesst,
> falls es mal notwendig sein sollte. Alle Spezialitaeten wie Threads,
> Synchronisationsmechanismen sind gekappselt, und existieren auch in
> einer Version fuer Windows, der Code selber enthaelt keine
> betriebssystemspezifischen Routinen.
Das wuerde ich jetzt als FUD bezeichnen.
Windows hat durchaus auch Prozess und Interprozesspipes, genauso wie VMS, MVS,
BeOS, ...
-Andi
Es gibt einen Unterschied zwischen dem, was moeglich und dem was sinnvoll ist.
Wenn irgendjemand schnelle Hacks macht, dann ist das ok fuer einen bestimmten
Zweck, aber das faellt fuer mich nicht unter ernsthaftes Programmieren. Und
nur weil Threads bei unsachgemaesser Behandlung Probleme machen koennen sind
sie nicht per se schlecht.
Oder, anderes Beispiel, selber Kontext: Nur weil C alle moeglichen
Schweinereien erlaubt ist Pascal nicht die bessere Sprache. Es kommt zum
weniger drauf an, was moeglich ist, sondern mehr, was man draus macht. Damit
verdienen wir ja schliesslich unser Geld (zumindest ich tue das).
> Windows hat durchaus auch Prozess und Interprozesspipes, genauso wie VMS, MVS,
> BeOS, ...
Darum geht es nicht (das war aus dem Kontext aber auch ersichtlich). Es ging
darum, ob es einen auf allen Betriebssystemen verfuegbaren Subset gibt, der
fuer alle praktischen Belange ausreichend ist. Bei Threads ist das definitiv
moeglich und ich arbeite mit solchen Bibliotheken. Bei anderen IPC Primitiven
war das, zumindest als ich es mir das letzte mal angeschaut habe, wesentlich
schwieriger.
Wie immer gilt natuerlich: Ich lasse mich gerne eines besseren belehren. Aber
nicht mit "Das ist FUD", sondern mit Hinweisen auf entsprechende Bibliotheken
oder Projekte. Ich verstehe genug von der Materie um durch Fakten ueberzeugt
zu werden:-)
> Andi Kleen <fre...@alancoxonachip.com> wrote:
> > Mit Threads und Shared memory ist es viel einfacher schnelle
> > Hackloesungen ohne viel Nachdenken einzubauen ("ich baue hier einfach mal
> > die Variable mit dem Lock rein -- wird schon funktionieren") und langfristig
> > faellt dann irgendwann alles zusammen.
>
> Es gibt einen Unterschied zwischen dem, was moeglich und dem was sinnvoll ist.
> Wenn irgendjemand schnelle Hacks macht, dann ist das ok fuer einen bestimmten
> Zweck, aber das faellt fuer mich nicht unter ernsthaftes Programmieren. Und
> nur weil Threads bei unsachgemaesser Behandlung Probleme machen koennen sind
> sie nicht per se schlecht.
Auch Leute die nicht nur schnelle Hacks machen haben genuegend Threadbugs.
Mit mehreren Prozessoren und portabler Software wird es nochmal
schlimmer (Schnell: sage die moeglichen Unterschiede in der Schreibreihenfolge
bei gesharten Daten ohne Memorybarriers zwischen Sparc64 und i386 auf)
Ich denke fuer die meiste Software sind sie schlecht, da sie einfach
zu schwierig zu debuggen sind. Kooperative Threads sind noch einigermassen
ertraeglich bzw komplett getrennte Prozesse mit einem sehr klar definierten
Interface (das kommt im Endeffekt meistens auf Nachrichten hinaus).
Explizite Nachrichten haben den Vorteil das das Verhalten einigermassen
mitlogbar und nachvollziehbar ist; bei den PosixThreads
Primitiven ist die Debugmoeglichkeit sehr schlecht bis gar nicht vorhanden
wenn du doch vorher mal ein Race uebersehen hast (und man uebersieht fast
immer ein paar)
>
> Oder, anderes Beispiel, selber Kontext: Nur weil C alle moeglichen
> Schweinereien erlaubt ist Pascal nicht die bessere Sprache. Es kommt zum
> weniger drauf an, was moeglich ist, sondern mehr, was man draus macht. Damit
> verdienen wir ja schliesslich unser Geld (zumindest ich tue das).
>
> > Windows hat durchaus auch Prozess und Interprozesspipes, genauso wie VMS, MVS,
> > BeOS, ...
>
> Darum geht es nicht (das war aus dem Kontext aber auch ersichtlich). Es ging
> darum, ob es einen auf allen Betriebssystemen verfuegbaren Subset gibt, der
> fuer alle praktischen Belange ausreichend ist. Bei Threads ist das definitiv
> moeglich und ich arbeite mit solchen Bibliotheken. Bei anderen IPC Primitiven
> war das, zumindest als ich es mir das letzte mal angeschaut habe, wesentlich
> schwieriger.
Ich glaube du wirst dir ziemlich schwertun, ein Betriebssystem zu finden,
das Prozesse, aber kein Equivalent zu einer Pipe hat. Mehr als eine Pipe ist
nicht noetig fuer Message passing. Die Behauptung das Prozesse weniger
portabel als Threads sind halte ich auch fuer ein Geruecht. Zwar kommt
man mit PosixThreads mit den meisten Betriebssystemen schon recht weit,
aber sobald du zB auf Windows portieren willst helfen sie dir auch nicht
mehr viel. Die paar Funktionen die fuer Prozessverwaltung und Pipes noetig
sind lassen sich stattdessen meiner Erfahrung nach fast ueberall
zusammenklauben; notfalls sogar auf PosixThreads aufgesetzt, ist aber
meistens nicht noetig. Umgekehrt wird es deutlich schwieriger.
-Andi (der zwar auch mit Threads arbeitet, sie aber fuer Teufelszeug haelt)
> Auch Leute die nicht nur schnelle Hacks machen haben genuegend
> Threadbugs. Mit mehreren Prozessoren und portabler Software wird es
> nochmal schlimmer (Schnell: sage die moeglichen Unterschiede in der
> Schreibreihenfolge bei gesharten Daten ohne Memorybarriers zwischen
> Sparc64 und i386 auf)
Diese Probleme existieren auch, wenn man mehrere Prozesse verwendet
und die Interprozeßkommunikation über eine gemeinsam genutzte
Speicherregion abwickelt. Insofern ist das kein thread-spezifisches
Problem.
> Explizite Nachrichten haben den Vorteil das das Verhalten einigermassen
> mitlogbar und nachvollziehbar ist;
Eben. Und ob man dann noch Threads oder Prozesse verwendet, ist dann
zweitrangig.
> Ich glaube du wirst dir ziemlich schwertun, ein Betriebssystem zu finden,
> das Prozesse, aber kein Equivalent zu einer Pipe hat.
Es gibt aber viele Betriebssysteme, bei denen das Anlegen eines
Prozesses relativ teuer ist.
Portabel heisst fuer micht so zu programmieren, dass ich mir eben *nicht*
ueber diese Unterschiede im klaren sein muss, bzw. nur sehr am Rande und nur
in speziellen Situationen. Wenn ich portabel C programmiere, dann verwende ich
Konstrukte, bei denen mir der Standard auf jeder Plattform garantiert, dass
sie funktionieren. Wie ist Sache der Compilerbauer bzw. derjenigen, die die
Standard-Bibliotheken schreiben. Wenn ich portabel mit Threads programmiere,
dann verwende ich eine Bibliothek, die mir einen vernuenftigen Subset der
verschiedenen Thread APIs bereitstellt und mir garantiert, dass dieser Subset
auf allen unterstuetzten Plattformen identisch ist.
> Ich denke fuer die meiste Software sind sie schlecht, da sie einfach
> zu schwierig zu debuggen sind. Kooperative Threads sind noch einigermassen
> ertraeglich bzw komplett getrennte Prozesse mit einem sehr klar definierten
> Interface (das kommt im Endeffekt meistens auf Nachrichten hinaus).
> Explizite Nachrichten haben den Vorteil das das Verhalten einigermassen
> mitlogbar und nachvollziehbar ist;
Ich verwende normalerweise Message Passing (Mailboxen) zwischen Threads, und
nur sehr eingeschraenkt Low Level Zeugs wie Semaphoren. Der Vorteil einer
vernuenftigen Bibliothek ist eben auch, dass sie Dir solche Sachen
plattformuebergreifend bietet. Klar, wenn Du grosse Programme mit mehreren
Threads in C mit statischen Daten und Mutexen zum Laufen kriegen willst, dann
hast Du ein Problem. Wenn ich die Schrauben an meinem Motorrad mit der
Rohrzange anziehe, statt mit dem Schraubenschluessel, dann liegt es nicht an
den Schrauben, wenn sie in Kuerze kaputt sind.
In Bezug auf Debugging hast Du auf der einen Seite recht. Es gibt definitiv
die wirklich aetzenden Fehler. Threads die aufgrund von Zeigerfehlern
sporadisch in Daten eines anderen Threads rumschreiben, und wo es nur dann
knallt, wenn das Programm ein bestimmtes Timing hat. Solche Sachen kennt
vermutlich jeder, der mit Threads programmiert.
Auf der anderen Seite ist es meiner Erfahrung nach auch wieder nicht so
schlimm, wie Du es darzustellen versuchst. Natuerlich musst Du Dir Hacks
verkneifen. Weitere Dinge, die sehr hilfreich sind, sind ein sauberes Design,
die Kombination von Threads mit einer vernuenftigen Bibliothek, so dass Du
nicht gezwungen bist, Low Level Primitve zu verwenden, und die Verwendung von
OO zum Kapseln von Threads und den zur Synchronisation notwendigen
Konstrukten. Mit diesen Hilfsmitteln (und natuerlich einen Mass Erfahrung
ueber die do's und dont's) habe ich mit Threads nicht entscheidend mehr
Probleme, als ohne (soll heissen: Die Vorteile ueberwiegen deutlich die
Nachteile).
> Ich glaube du wirst dir ziemlich schwertun, ein Betriebssystem zu finden,
> das Prozesse, aber kein Equivalent zu einer Pipe hat. Mehr als eine Pipe ist
> nicht noetig fuer Message passing. Die Behauptung das Prozesse weniger
> portabel als Threads sind halte ich auch fuer ein Geruecht. Zwar kommt
> man mit PosixThreads mit den meisten Betriebssystemen schon recht weit,
> aber sobald du zB auf Windows portieren willst helfen sie dir auch nicht
> mehr viel. Die paar Funktionen die fuer Prozessverwaltung und Pipes noetig
> sind lassen sich stattdessen meiner Erfahrung nach fast ueberall
> zusammenklauben; notfalls sogar auf PosixThreads aufgesetzt, ist aber
> meistens nicht noetig. Umgekehrt wird es deutlich schwieriger.
Ich habe seit Jahren eine Bibliothek in Benutzung, die mir fuer Posix-Threads,
sowie native Threads unter Windows und OS/2 (das sind die Plattformen, die ich
normalerweis brauche) alle notwendigen Dinge in OO Manier zur Verfuegung
stellt. Ich habe (teilweise mit anderen zusammen) recht grosse
Softwareprojekte fuer industrielle Anwender gemacht. Portabilitaet zwischen
den verschiedenen Plattformen war dabei (was das Multithreading API angeht)
nie ein Thema, weil alle Unterschiede nur in dieser Bibliothek stecken, und
nach aussen hin nicht sichtbar sind. Insofern ist das was ich das sage keine
theoretische Rumschwallerei, sondern Real Life. Es gibt uebrigens nicht nur
eine solche Bibliothek, sondern diverse, schau Dir zum Beispiel mal
http://www.cs.wustl.edu/~schmidt/ACE-overview.html
an. Und deshalb kannst Du das, was ich sage, gerne fuer ein Geruecht halten,
die Realitaet spricht aber eine deutlich andere Sprache.
Ich habe die von mir verwendete Bibliothek fuer Threads selber geschrieben und
habe mir in dem Zusammenhang auch angeschaut, ob es mit ueberschaubarem
Aufwand moeglich waere, etwas aehnliches fuer Prozesse zu schreiben. Ich habe
damals davon abgesehen, weil ich nach einigem Studium die Machbarkeit
angezweifelt habe. Ich kenne auch keine solche Bibliothek. (Bei Apache 2.0
soll ein Thread/Prozess Layer dabei sein, ich hab's mir aber noch nicht
angeschaut und weiss nicht ob es - unabhaengig von Apache - universell
verwendbar ist. Ausserdem ist es reines C, fuer meine Zwecke also nicht so gut
geeignet.)
Wie ich bereits in meinem letzten Posting geschrieben habe, wurde ich mich
gerne eines besseren belehren lassen - nur muessen dazu von Dir Beispiele
kommen. Es ist nicht so, dass ich nie separate Prozesse verwende (falls ja,
dann aber meist mit Kommunikation ueber IP, damit die unterschiedlichen
Prozesse notfalls auch auf verschiedenen Maschinen laufen koennen). Insofern
haette ich auch was dazugelernt, wenn Du mir entsprechende Hinweise liefern
koenntest. Spekulationen ueber Geruechte und FUD helfen mir aber nicht weiter.
Oh, ich verstehe, Ullrich ist so ein erfahrener Programmierer, daß er
auf exotische Randfunktionalität wie malloc() in seinen Programmen
verzichtet. Wer malloc() benutzt, ist halt selber schuld und hat sich
seine Reentranzprobleme selber gemacht.
Oder kommst du jetzt und definierst das Problem der libc zu?
Reentranszprobleme gibt es nicht, weil es das Problem der armen
Schweine von der libc ist?
> Wenn einer Deiner Prozesse abraucht steht das System genauso.
Was für eine hanebüchene Lüge!
> Und ich wuerde in beiden Faellen Vorkehrungen treffen, den oder die
> Prozesse wieder zu starten - Threads oder nicht.
Oh, du würdest Vorkehrungen treffen. Warst du nicht der Philister, der
hier herumpupte, die ganzen Probleme bei Threads seien kein Thema, weil
sie eh nicht aufträgen, wenn man perfekten und fehlerfreien Code
schreibt?
> Wenn ich Threads verwende, dann nicht aus Performance-Gruenden, sondern weil
> sie die Kommunikation zwischen den verschiedenen Teilen des Programms leichter
> machen, und deshalb das Design vereinfachen.
In deinem Paralleluniversum gibt es sicher auch keine Steuern und alle
Leute benutzen NeXTSTEP, gell?
> * Threads erlauben eine einfachere Abstimmung zwischen den verschiedenen
> Teilen des Programms.
Das Gegenteil ist der Fall.
> * Sie sind beim heutigen Stand der Technik portabler.
Das Gegenteil ist der Fall.
> Ich habe es nicht probiert, weil ich nicht dafuer bezahlt wurde,
> aber ich gehe davon aus, dass sich mein Programm in etwa einem Tag
> auf Windows portieren laesst, falls es mal notwendig sein sollte.
Ah, du hast keine Ahnung, aber du nimmst mal an. Das Argument überzeugt
sofort durch seine Stichhaltigkeit.
> Alle Spezialitaeten wie Threads, Synchronisationsmechanismen sind
> gekappselt, und existieren auch in einer Version fuer Windows, der
> Code selber enthaelt keine betriebssystemspezifischen Routinen.
Oi, das performt sicher ganz toll.
> Warum gehst Du nicht einfach auf meinen Einwand ein? Ich habe Dir erklaert,
> warum meine Massnahme Sinn macht.
Nein.
> Ein Server mit dieser Aenderung laeuft auch bei wechselnden Lasten auf
> dem Server performanter als einer ohne.
Nein.
> Es geht auch nicht darum, dass irgendein Dienst mal Vollast faehrt,
> oder dass das System zusammenbricht. Funktionieren tut Deine und meine
> Variante.
Nur daß deine Variante viel mehr Systemressourcen frißt, langsamer und
schlechter wartbar ist.
> Wenn aber der Admin mal einen Backup im Hintergrund macht
> oder sonstwas lauft, dann nutzt meine Version die zur Verfuegung
> stehenden Resourcen moeglichst gut aus,
So ein Unfug! Dieser Schwachsinn wird nicht glaubwürdiger, wenn du ihn
ein paar Dutzend Mal wiederholst!
> Wenn ich mich recht erinnere, dann ist das einer der Unterschiede zwischen
> gutem und nicht so gutem Design.
Bei dir sehe ich überhaupt kein Design, sondern nur Gefasel, und zudem
nicht sehr überzeugendes.
Zeig doch mal Software von dir her.
Felix
Deine Widerlegungsversuche kommen gleich viel besser an, wenn du diese
Aussagen widerlegst, die vorher von anderen getroffen wurden. Und die
Aussage in diesem Fall war: Threads sind überflüssig, weil man mit ihnen
nichts machen kann, was man nicht auch ohne Threads machen kann, und
weil sie Code schlechter wartbar machen.
Deine gesamten "Argumente", wieso Threads nett sind, laufe darauf
hinaus:
a. "ich hab ne tolle Wrapper-Library, die macht das für mich"
b. "man darf natürlich die ganzen Fehler nicht machen, die alle Leute
mit Threads machen"
Unter dieser Prämisse sind auch verteilte militärische Datenbanken mit
100 gleichberechtigten Knoten ohne hierarchische Struktur kein Problem.
Das sind keine sinnvollen Prämissen, um eine Technologie zu evaluieren.
> Darum geht es nicht (das war aus dem Kontext aber auch ersichtlich). Es ging
> darum, ob es einen auf allen Betriebssystemen verfuegbaren Subset gibt, der
> fuer alle praktischen Belange ausreichend ist.
Ja, und das ist bei Windows so, wie Andi schrieb.
> Bei Threads ist das definitiv moeglich und ich arbeite mit solchen
> Bibliotheken. Bei anderen IPC Primitiven war das, zumindest als ich es
> mir das letzte mal angeschaut habe, wesentlich schwieriger.
Nein. Wenn du, wie du das hier tust, eine perfekte Wrapper-Bibliothek
postulierst, ist das natürlich exakt genau so einfach und funktioniert
immer.
> Wie immer gilt natuerlich: Ich lasse mich gerne eines besseren belehren. Aber
> nicht mit "Das ist FUD", sondern mit Hinweisen auf entsprechende Bibliotheken
> oder Projekte. Ich verstehe genug von der Materie um durch Fakten ueberzeugt
> zu werden:-)
Soso.
Andreas Ferber <afe...@techfak.uni-bielefeld.de> wrote:
> * Ingo Oeser <i...@informatik.tu-chemnitz.de> schrieb:
[...]
> Bei Shared Memory hast du gegenueber Threads
> ja nur einen ganz bestimmten Ueberschneidungspunkt zwischen den
> Prozessen, und nur beim Zugriff darauf musst du besondere Vorsicht
> walten lassen.
Aber du hast die gleiche Synchronisationsmethode nur halt mit der
worst-case-Implementierung vorgeschlagen. Ich sprach ja auch von
der Methode Shared Memory + (SysV-) Semaphore als
Verschlimmbesserung und habe deswegen Alternativen vorgeschlagen.
> Dann musst du aber wieder mit select()/poll() arbeiten (oder du
> verzichtest auf die Speicherqueue und verwirfst Pakete, wenn alle Pipes
> EAGAIN liefern). Ich hatte bei meiner Shared Memory/Semaphoren-Idee
> eigentlich im Hinterkopf, dasz man dann einfach mit einem blockierenden
> Socket arbeiten kann.
Ich hatte Performance und Syscall-Reduktion im Hinterkopf.
Deine Idee ist die Lehrbuchmethode und somit auf jeden Fall
erstmal absolut korrekt. Ich wollte nur zeigen, was bei Unix
sonst noch so sinnvoll funktioniert und was insbesondere mit
Threads dann nicht oder nur schwer funktioniert, um dem OP die
zusaetzlichen Vorteile von Prozessen (nebst Speicherschutz und
einfacheren Flows) nahezubringen.
> Ausserdem ist dieser Ansatz unguenstig, wenn die Bearbeitungszeiten der
> einzelnen Pakete stark schwanken.
Das hatte ich ausser acht gelassen. Man kann sowas aber durch
genuegend Prozesse und Timestamps (machen MSG-Queues
automatisch!) anfangen. Das "Wie?" kann ich bei Bedarf ausfuehren.
>> Ohne diese Queue wuerde sich allerdings ein natuerliches
>> Load-Management ergeben ;-)
> Ack. Aber nach Aussage des OP ist die Latenz ja egal, so dasz ich
> derartige Ueberlegungen erstmal ausgeklammert habe.
Aber wir wollen auf keinen Fall eine DoS -> Wir brauchen ein
Load-Management immer bei einem Server.
> Du musst sowieso ein Programm schreiben, das Testpakete generiert, ob
> diese dann nach stdout geschrieben werden oder im Shared Memory abgelegt
> werden ist IMHO nebensaechlich ;-)
Aehm, ob ich das mit netcat und einem Shell-Skript oder als
C-Programm mache ist fuer mich ein signifikanter Unterschied ;-)
> Ack, an Message Queues hatte ich nicht gedacht. Allerdings kann ein
> UDP-Paket groesser als MSGMAX sein.
Man hat ja MSG-Nummern. Da kann man Fragmente mit basteln
(pervers, aber moeglich). Oder man akzeptiert keine UDP-Pakete >
MSGMAX [1] und sieht diese Grenze bereits im Protokoll vor.
> Denkbar waere noch ein kombinierter Einsatz von Shared Memory und Message
> Queue: der Receiver legt ein Paket im Shared Memory ab und versendet
> ueber die Message Queue eine Message mit der Slotnummer des neuen
> Paketes.
Wenn du das jetzt auch noch abhaengig von der Groesze des Paketes
machst, hast du fast den Basis-IPC-Mechanismus von NT ;-)
> Um einen Slot wieder freizugeben, muss der Worker dann ein mit
> dem Slot assoziiertes Flag im Shared Memory auf Null setzen (das geht
> atomar), belegt werden Slots nur vom Receiver.
Nee, das kann man auch gleich ueber Messages machen. Atomare
Operationen im Userspace gibt es nicht auf jeder Architektur. Und
wenn man jetzt Semaphoren einsetzt, hat man wieder nichts gewonnen.
Dafuer gibt es den "Send-Receive-Reply" Mechanismus bei Messages.
[Realisierung des Slottransfers mittels pipes]
Auch eine Idee. Aber ich wuerde die Mechanismen nicht zu sehr
mixen, weil das die Komplexitaet erhoeht und zuviele IDs
alloziert und verwaltet werden muessen.
>> Das wiederum kann man durch Zusammenfassen von kleinen zu groszen
>> Paketen erreichen. Man schaltet dann eben erst nach 100K
>> empfangenden Daten oder einer Sekunde um, je nachdem was frueher
>> passiert.
> Das bringt bei schwankenden Bearbeitungszeiten wieder die gleichen
> Probleme wie bei den Pipes oben.
Die Worker sollten das ausgleichen. Bei stark schwankenden
Bearbeitungszeiten ist ein Redesign faellig.
>> Die Semaphoren des Systems zu benutzen ist auf jeden Fall extrem
>> langsam. So langsam, dass ein zusaetzlices rumkopieren da meist
>> nicht auffaellt. Meist bieten die Thread-Pakete da irgendwas
>> schnelleres, wenn es innerhalb des selben Adreszraumes liegt.
> Da wird es dann aber schnell architekturabhaengig.
Nein, lies den Abschnitt von mir nochmal. Das Thread-Paket
abstrahiert das i.A. ausreichend, wenn der Programmierer weiter
als bis zu seiner Nasenspitze gedacht hat. Der worst-case ist
dann die Benutzung der Semaphoren des OS, welches dem von dir
vorgeschlagenem Mechanismus entspricht ;-)
> Insbesondere waere es auch interessant zu wissen, wie gross die
> UDP-Datagramme werden (s.o.) ;-)
Ja, der OP sollte hier mal genauer werden.
- Maximum und Minimum der Bearbeitungszeit
- Groesze der Pakete
- Maximale Laenge des Backlogs
- ...
Sicher liest der aber gar nicht mehr mit ;-)
MfG
Ingo Oeser
[1] Ich finde momentan das garantierte Minimum fuer MSGMAX nicht.
Kann das mal jmd. ergaenzen? Danke!
Wenn ich jemand einen Rewrite unter Windows mit zwei Saetzen
ersparen kann, gehe ich auf solcherlei Probleme ein. Das hat
etwas mit Hoeflichkeit zu tun (die uebrigens auch von der
Netiquette gefordert wird).
>> Dein Einwand von unten (Sack voller Aerger) ist dann aber immer
>> noch berechtigt und sicher ein Grund fuer diverse Instabilitaeten
>> und Megaprogramme.
> Multithreading ist und bleibt eine schlechte Idee, und es gibt auszer der
> miserablen fork()-Performance von Solaris keinen Grund, sich (auch nur
> kurzzeitig) mit Threads auseinanderzusetzen.
Wo siehst du zwischen diesen Abschnitten den Widerspruch?
Im Userspace ist multithreading tatsaechlich Unsinn in 99% der
Faelle, in denen es eingesetzt wird.
> Andi Kleen <fre...@alancoxonachip.com> writes:
>
> > Auch Leute die nicht nur schnelle Hacks machen haben genuegend
> > Threadbugs. Mit mehreren Prozessoren und portabler Software wird es
> > nochmal schlimmer (Schnell: sage die moeglichen Unterschiede in der
> > Schreibreihenfolge bei gesharten Daten ohne Memorybarriers zwischen
> > Sparc64 und i386 auf)
>
> Diese Probleme existieren auch, wenn man mehrere Prozesse verwendet
> und die Interprozeßkommunikation über eine gemeinsam genutzte
> Speicherregion abwickelt. Insofern ist das kein thread-spezifisches
> Problem.
shm ist aber unbequem genug das es sich jeder 2-3x ueberlegt bevor er es
benutzt. Das ist eine gute Sache.
> > Ich glaube du wirst dir ziemlich schwertun, ein Betriebssystem zu finden,
> > das Prozesse, aber kein Equivalent zu einer Pipe hat.
>
> Es gibt aber viele Betriebssysteme, bei denen das Anlegen eines
> Prozesses relativ teuer ist.
Anlegen ist relativ uninteressant. Wenn zB dein Netzwerkserver fuer
jede neue Verbindung einen neuen Thread erstellt, hast du sowohl mit
Prozessen als auch mit Threads ein Performanceproblem. Ernsthafte
Software arbeitet deswegen mit Threadpools wo nur ganz selten ein
neuer erstellt wird, oder sie baut nur selten Verbindungen auf. Wie
lange es dann dauert einen Thread oder Prozess zu erzeugen ist
deswegen relativ uninteressant.
Interessanter ist die Contextswitchzeit. Die ist zwischen Prozessen
zwar schlechter als zwischen Threads, der Unterschied ist aber im Allgemeinen
schwierig bei echten Applikationen herauszubenchmarken.
Es gibt auch aber Operationen die mit Threads deutlich teurer sind als
bei Prozessen. Alles was zB die Speichermappings aendert (mmap etc.)
muss an alle CPUs die einen Thread vor dir laufen haben koennten einen
InterProcessorInterrupt schicken um die TLBs zu loeschen. Das ist sehr
langsam und kann ein Problem werden wenn du viel Speicher allozierst etc.
Prozesse laufen dafuer im allgemeinen komplett lokal auf einer CPU und
skalieren deswegen in diesen Faellen besser.
-Andi
Ich kenne ehrlich gesagt niemanden, der freiwillig mal eben
$MONSTERAPPLIKATION nach Windoze zu portieren versucht.
> Das hat etwas mit Hoeflichkeit zu tun (die uebrigens auch von der
> Netiquette gefordert wird).
Da liegt eigentlich immer ein Zwang vor. Klar, wenn du meinst, daß du
jemandem helfen kannst, tu es. Würde mich wundern, wenn sich uz mit
Argumenten überzeugen ließe.
> >> Dein Einwand von unten (Sack voller Aerger) ist dann aber immer
> >> noch berechtigt und sicher ein Grund fuer diverse Instabilitaeten
> >> und Megaprogramme.
> > Multithreading ist und bleibt eine schlechte Idee, und es gibt auszer der
> > miserablen fork()-Performance von Solaris keinen Grund, sich (auch nur
> > kurzzeitig) mit Threads auseinanderzusetzen.
> Wo siehst du zwischen diesen Abschnitten den Widerspruch?
Gar nicht, ich wollte das nur noch mal gesagt haben ;)
Felix
--
Lästert ihr nur, ihr frechen Schnösel - ich merk mir das...
--Helmut Schellong ist ein schlechter Verlierer in dcous
> Ich kenne ehrlich gesagt niemanden, der freiwillig mal eben
> $MONSTERAPPLIKATION nach Windoze zu portieren versucht.
Mit Interix soll das ja nicht so schwierig sein...
War Interix nicht das Toolkit, das am Internet Exploder für Unix schuld
war?
Ok, schau dir den Linux Kernel an und verfolge mal einen Monat
die linux-mm-Mailingliste. Oder schau dir einfach mal das Archiv
an. Noch besser ist es, sich gleich mal linux-kernel einen Monat
anzusehen.
Da siehst du die Folgen von threads sehr genau. Der Kernel
arbeitet naemlich sobald er im Kernel-Modus ist (und er auf
globale Daten zugreifen kann) im Prinzip mit Threads.
Andi kennt diese Listen und liest die groessere davon. Es ist
also genauso praktisch wie dein Zeugs und hat NICHTS mit
Geruechten zu tun.
Das der C-Compiler einen da wenigstens in Bezug auf atomare
Operationen unterstuetzen koennte, ist allerdings wahr *seufz*
MfG
Ingo Oeser
Du hast nicht verstanden: Das Wort "Gefahr" definiert, dass die
_Moeglichkeit_ besteht, das etwas schief laeuft und nicht per se,
das etwas schief laeuft. Erst Murphy definiert das so ;-)
Der Einsatz von Threads ist _gefaehrlich_. Gefahren gilt es zu
minimieren bei ernstgemeinten Anwendungen, weil Murphy in der
Praxis recht hat.
Wenn du solche Minimierung von Gefahren nicht benoetigst, weil du
so ein genialer Programmierer bist, das dir nie, aber auch
wirklich nie Fehler unterlaufen, nie races eingebaut werden und
alle Programmierer von allen Libraries, die du so verwendest
genauso fehlerfrei arbeiten, dann und NUR dann geb ich dir recht.
> Oder, anderes Beispiel, selber Kontext: Nur weil C alle moeglichen
> Schweinereien erlaubt ist Pascal nicht die bessere Sprache.
C wird genommen, weil es da meist bessere und preiswertere
Compiler fuer fast alle Architekturen gab. Die Sprache an sich
hat sehr viele Maengel. Ausserdem sind viel Unix-APIs sehr
C-freundlich geschaffen. Ein Pascal-Port muss ja immer die ganzen
Header umbasteln. Deswegen nimmt das keiner fuer systemnahe
Sachen. Das ganze schleicht sich dann bis zum GUI-Tool durch.
Ich sag nur: Been there, done that.
Das ist was anderes, weil der Kernel gleichzeitig derjenige ist, der Threads
implementiert. Zudem ist es ein stark monolithischer Kernel. Beides zusammen
bedeutet, dass oft Zuflucht zu Low Level Konstrukten wie Semaphoren genommen
werden muss. Solche Konstrukte sind kritisch, das habe ich nie abgestritten.
Ein Benutzer-Programm kann aber problemlos und ohne grossen
Performance-Verlust auf Message Passing Konzepte aufsetzen, was als Resultat
normalerweise gut wartbare und relativ unkritische Programme ergibt (zumindest
was das Threading angeht). Ich werde als Antwort auf Deinen anderen Artikel
nochmal genauer darauf eingehen.
Alternativ könnte er sich auch einfach mal LinuxThreads oder die
libpthread der diet libc anschauen und verstehen. Ich glaube nicht, daß
ihm letzteres gelingen wird, nach seinen anderen Aussagen hier zu
urteilen, aber wer verstanden hat, wie Userspace Threads unter Linux
umgesetzt werden und mit welchen Mitteln (wenn überhaupt) Anforderungen
des Standards umgesetzt werden, der wird alleine deshalb nie wieder
Threads anfassen.
Im Übrigen ist zu Threads alles sagenswerte gesagt. Andi kann da sicher
noch viel mehr ganz tolle Anekdoten bringen, aber das ist vielleicht
eher für de.comp.os.unix.recovery (RfD now!) ;)
Felix
Aha, so ist das also.
> Zudem ist es ein stark monolithischer Kernel.
Aha, jetzt wo du es sagst... hier spricht offensichtlich ein Experte.
Das ist wirklich kühn. Herr von Bassewitz erklärt Andi Kleen, was wir
für einen Kernel haben. Unglaublich.
> Beides zusammen bedeutet, dass oft Zuflucht zu Low Level Konstrukten
> wie Semaphoren genommen werden muss.
Du bist wirklich eine unglaubliche Knalltüte!
Was hälst du denn für High-Level Konstrukte bei Threads?
> Solche Konstrukte sind kritisch, das habe ich nie abgestritten.
Nein, du hast hier nur gehaltloses Gefasel wie "ich mach das seit
Jahren, also muß das gut sein" gebracht.
> Ein Benutzer-Programm kann aber problemlos und ohne grossen
> Performance-Verlust auf Message Passing Konzepte aufsetzen, was als
> Resultat normalerweise gut wartbare und relativ unkritische Programme
> ergibt (zumindest was das Threading angeht). Ich werde als Antwort auf
> Deinen anderen Artikel nochmal genauer darauf eingehen.
Das kannst du dir auch gerne sparen.
Es würde mehr Sinn machen, wenn du uns an deinen fraglos ebenfalls
beträchtlichen Erkenntnissen über Supraleitung oder kalte Fusion
teilhaben läßt. Tu das aber bitte in einer anderen Newsgroup.
Felix
Und wie bei allen Dingen die gefaehrlich sind, beeinflusst derjenige, der sich
der Gefahr aussetzt in einem sehr grossen Masse, ob irgendwas nur gefaehrlich
aussieht, oder ob es wirklich gefaehrlich ist.
> Wenn du solche Minimierung von Gefahren nicht benoetigst, weil du
> so ein genialer Programmierer bist, das dir nie, aber auch
> wirklich nie Fehler unterlaufen, nie races eingebaut werden und
> alle Programmierer von allen Libraries, die du so verwendest
> genauso fehlerfrei arbeiten, dann und NUR dann geb ich dir recht.
Ich bin kein genialer Programmierer (leider) und ich baue genauso Fehler in
meine Programme ein wie andere Leute (auch wenn ich, was Fehleranzahl pro
Codemenge angeht normalerweise ganz gut liege). Es kommt aber entscheidend
darauf an, wie man mit Gefahren umgeht. Wer ohne Vorbereitung den Salto
Mortale wagt ist einfach nur dumm, wer sich dagegen gut vorbereitet, fuer den
ist die damit verbundene Gefahr absolut kalkulierbar und er kann problemlos
damit seinen Lebensunterhalt damit verdienen.
Neben der Verwendung von High Level Konstrukten (wo moeglich) gibt es noch
mehr Sachen, die einem bei Threads das Leben erleichtern. Eine davon ist die
Kombination mit einer der gaengigen OO Sprachen (bei mir ist das C++).
Interessanterweise haben gute C++ Programme naemlich Eigenschaften, die die
Gefahren von Threads minimieren:
* Wenn Threads als Objekte dargestellt werden, dann sind private Daten einer
Instanz automatisch nicht von aussen zugaenglich (keine
Reentranzprobleme).
* Wenn ein Thread ein Objekt ist, dann existieren diese Daten jeweils
automatisch per Thread und sind damit wesentlich ungefaehrlicher als
globale Variablen (die in C++ sowieso verpoent sind - ein weiterer
Vorteil).
* Wer get/set Methoden zum Zugriff auf Instanzdaten verwendet, der hat
automatisch bereits die Schnittstellen geschaffen, an denen man notfalls
mit Mutexen Resource Locking betreiben muss.
* Mutex Locking laesst sich in ein Objekt verpacken, damit sorgt der
Compiler automatisch dafuer, dass Mutexe beim Verlassen eines Blocks
wieder entriegelt werden:
class ResourceLock {
Mutex& M;
public:
ResourceLock (Mutex& aM): M (aM) { M.Lock(); }
~ResourceLock () { M.Unlock(); }
}
Um das, was ich sage mal zu demonstrieren habe ich 50 Zeilen Code
zusammengehackt. Der Code ist weder getestet, noch komplett, aber er zeigt in
ungefaehr, wie man mit Hilfe einer vernuenftigen Library in zwei Minuten ein
Programm mit mehreren Threads schreiben kann. Wer sich dafuer interessiert
kann einen Blick auf http://www.biofix.de/thread.cc werfen (ich wollte
diejenigen, die sich dafuer nicht interessieren nicht damit quaelen, deshalb
habe ich den Code im Web abgelegt).
Ein paar Anmerkungen dazu:
* Der Code ist nicht getestet.
* Die Verwendung von printf funktioniert bei mehreren Threads
selbstverstaendlich nicht. Es handelt sich um ein Beispiel (eigentlich
will ich an der Stelle auch auf was ganz anderes raus - s.u.).
* Es gibt natuerlich ein paar Stellen, an denen man das Programm verbessern
kann, die habe ich den allseits bekannten Rosinenpickern gelassen, damit
sie was zu maulen haben:-)
* Die Implementation der Thread-Klasse gehoert natuerlich gekapselt in ein
eigenes Modul. Wie sie implementiert ist, ist fuer das Hauptprogramm
unwichtig.
* Der Code ist automatisch portabel. Alle verwendeten Konstrukte existieren
in identischer Form fuer alle gaengigen Betriebssysteme (wobei "gaengig"
hier natuerlich meine Version von "gaengig" ist).
* Es ist absolut einfach, mehrere Arbeiter-Threads zu erzeugen, und den Code
damit skalierbar zu machen: Mehrfach "new MyThread" und das war's (es muss
zum Beenden natuerlich die entsprechende Anzahl Shutdown Nachrichten in
der Mailbox abgelegt werden).
Wenn Du Dir den Code mal anschaust, dann wirst Du vermutlich ein paar Sachen
merken:
1. Der Code enthaelt nur High-Level Konstrukte. Semaphoren usw. sind nicht
notwendig.
2. Der Code ist kurz und absolut uebersichtlich, es gibt keine Stelle, an
der Race-Conditions auftreten koennen (ok, es gibt eine, aber die wird
den meisten nicht auffallen, und sie spielt in der Praxis keine grosse
Rolle. Sie liesse sich zudem mit wenigen Zeilen mehr beheben - ich wollte
bloss das Programm nicht unnoetig komplizieren).
Und wenn Du dann noch einen zweiten Blick auf den Code wirfst, wird Dir
vielleicht auffallen, dass diese laepischen 50 Zeilen Code bereits alles sind,
was Du (thread-technisch) fuer die vom OP gestellte Aufgabe brauchst: Aus dem
PrintJob wird ein CryptJob, und das Job Objekt enthaelt die Daten, die vom
Socket gelesen wurden. Natuerlich kann man noch ein paar Schleifchen
dranmachen: Ich wuerde zum Beispiel das Lesen vom Socket nicht im
Hauptprogramm machen, sondern nochmal einen Thread dafuer "opfern", und dafuer
im Hauptprogramm dediziert Signale abhandeln. Das sind aber wie gesagt
Schleifchen - im Prinzip reichen die paar von mir geschriebenen Zeilen bereits
komplett aus. Mehrere Prozesse zu verwenden ist nicht sicherer und mit Kanonen
auf Spatzen geschossen. In der Zeit, die ich fuer die paar Zeilen benoetigt
habe hast Du vermutlich noch nicht mal Deine diversen Makefiles geschrieben.
So, und jetzt bist Du wieder dran: Wo genau lauern Deiner Meinung nach im
Beispielcode die Gefahren der Thread-Programmierung?
Selbstverständlich hilft die Benutzung einer OO-Sprache GENAU GAR NICHT
dabei, die Reentranzprobleme zu beseitigen.
Man kann in C++ wie auch in anderen Sprachen Konstrukte finden, die
einem die Arbeit partiell erleichtern, aber das war's auch schon.
Kurz: du sonderst mal wieder FUD ab.
> * Wenn Threads als Objekte dargestellt werden, dann sind private Daten einer
> Instanz automatisch nicht von aussen zugaenglich (keine
> Reentranzprobleme).
Oh Mann! Diese völlige Ahnungslosigkeit muß doch schmerzhaft sein?!
man static
> * Wenn ein Thread ein Objekt ist, dann existieren diese Daten jeweils
> automatisch per Thread und sind damit wesentlich ungefaehrlicher als
> globale Variablen (die in C++ sowieso verpoent sind - ein weiterer
> Vorteil).
"verpönt", wie? Grober Unfug.
> * Wer get/set Methoden zum Zugriff auf Instanzdaten verwendet, der hat
> automatisch bereits die Schnittstellen geschaffen, an denen man notfalls
> mit Mutexen Resource Locking betreiben muss.
get/set... Boah, ich sehe deinen Code förmlich vor mir.
Performt nicht, ist nicht les- oder wartbar. Ganz prima.
man operator overloading
> * Mutex Locking laesst sich in ein Objekt verpacken, damit sorgt der
> Compiler automatisch dafuer, dass Mutexe beim Verlassen eines Blocks
> wieder entriegelt werden:
> class ResourceLock {
> Mutex& M;
> public:
> ResourceLock (Mutex& aM): M (aM) { M.Lock(); }
> ~ResourceLock () { M.Unlock(); }
> }
Meine Güte, was für eine Erkenntnis! Wann habe ich das zum ersten Mal
gesehen? 1993?
> * Die Verwendung von printf funktioniert bei mehreren Threads
> selbstverstaendlich nicht. Es handelt sich um ein Beispiel (eigentlich
> will ich an der Stelle auch auf was ganz anderes raus - s.u.).
Uh, _wie bitte_?!
Deine Äußerungen sind ja grotesk!
> * Die Implementation der Thread-Klasse gehoert natuerlich gekapselt in ein
> eigenes Modul. Wie sie implementiert ist, ist fuer das Hauptprogramm
> unwichtig.
C++? "Eigenes Modul"?
Oh Mann. Du hättest echt bei Turbo Pascal bleiben sollen.
> * Der Code ist automatisch portabel. Alle verwendeten Konstrukte existieren
> in identischer Form fuer alle gaengigen Betriebssysteme (wobei "gaengig"
> hier natuerlich meine Version von "gaengig" ist).
Wieso preist du hier ständig deine "Lösungen" zu Problemen an, die du
selbst geschaffen hast? Mein Code braucht _gar_ keine Thread-Portabilitätslayer!
> * Es ist absolut einfach, mehrere Arbeiter-Threads zu erzeugen, und den Code
> damit skalierbar zu machen:
Deine Vorstellungen von Skalierbarkeit decken sich offensichtlich nicht
mit dem Rest der IT-Industrie.
> Wenn Du Dir den Code mal anschaust, dann wirst Du vermutlich ein paar Sachen
> merken:
> 1. Der Code enthaelt nur High-Level Konstrukte. Semaphoren usw. sind nicht
> notwendig.
Semaphoren sind nicht High Level.
Aber das sagte ich ja bereits. Wahrscheinlich ist das zu hoch für dich.
Und neues Wissen braucht jemand mit deiner Perfektion ja nicht.
> 2. Der Code ist kurz und absolut uebersichtlich, es gibt keine Stelle, an
> der Race-Conditions auftreten koennen (ok, es gibt eine, aber die wird
> den meisten nicht auffallen, und sie spielt in der Praxis keine grosse
> Rolle. Sie liesse sich zudem mit wenigen Zeilen mehr beheben - ich wollte
> bloss das Programm nicht unnoetig komplizieren).
Das läßt ja wirklich tief blicken.
Felix
> Was hälst du denn für High-Level Konstrukte bei Threads?
Rendezvous.
> * Wenn Threads als Objekte dargestellt werden, dann sind private Daten einer
> Instanz automatisch nicht von aussen zugaenglich (keine
> Reentranzprobleme).
Dies gilt für Tasks in Ada. Dort muß man Zeiger auf lokale Daten schon
mit gewalt exportieren. Für C++ ist das in meinen Augen ein
übertrieben optimistischer Ansatz.
> * Wenn ein Thread ein Objekt ist, dann existieren diese Daten jeweils
> automatisch per Thread und sind damit wesentlich ungefaehrlicher als
> globale Variablen (die in C++ sowieso verpoent sind - ein weiterer
> Vorteil).
Wie war das noch mal mit dem Singleton Pattern?
> * Wer get/set Methoden zum Zugriff auf Instanzdaten verwendet, der hat
> automatisch bereits die Schnittstellen geschaffen, an denen man notfalls
> mit Mutexen Resource Locking betreiben muss.
get/set-Methoden sind dafür nicht hinreichend, es braucht ein
vernünftiges Design, da typischerweise mehrere set-Operationen en bloc
durchgeführt werden müssen.
> * Mutex Locking laesst sich in ein Objekt verpacken, damit sorgt der
> Compiler automatisch dafuer, dass Mutexe beim Verlassen eines Blocks
> wieder entriegelt werden:
Wer so etwas regelmäßig macht, erzeugt Code, der sich nie auf andere
OO-Sprachen portieren läßt (außer Ada, welches noch am ehesten das
Objektmodell von C++ implementiert). Ob man das will?
> Um das, was ich sage mal zu demonstrieren habe ich 50 Zeilen Code
> zusammengehackt.
Dieses Verfahren führt bei vielen Systemen zu Problemen, da die
Kommunikation im wesentlichen nur in eine Richtung abläuft.
Ferner geht aus dem Codeabschnitt nicht hervor, wie Fehler in den Jobs
behandelt werden, was passiert, wenn die Mailbox überläuft (ob sie das
überhaupt tut) oder wenn RequestStop() zweimal aufgerufen wird.
Außerdem dürfte der Nachweis schwer werden, daß für ein gegebenes
Problem die Jobs fair verteilt werden (insbesondere wenn die Jobs
selbst wieder neue Jobs generieren).
> * Der Code ist automatisch portabel.
Das ist nicht korrekt. Die C++-Semantik unterstützt keine Threads.
> Alle verwendeten Konstrukte existieren in identischer Form fuer
> alle gaengigen Betriebssysteme (wobei "gaengig" hier natuerlich
> meine Version von "gaengig" ist).
Achso.
> Wenn Du Dir den Code mal anschaust, dann wirst Du vermutlich ein paar Sachen
> merken:
>
> 1. Der Code enthaelt nur High-Level Konstrukte. Semaphoren usw. sind nicht
> notwendig.
In einer typischen Anwendung, bei der Datenfluß nicht ein Sturzbach in
eine Richtung ist, wirst Du Muster auf noch höherer Ebene
zusammenstricken müssen.
> Und wenn Du dann noch einen zweiten Blick auf den Code wirfst, wird
> Dir vielleicht auffallen, dass diese laepischen 50 Zeilen Code
> bereits alles sind, was Du (thread-technisch) fuer die vom OP
> gestellte Aufgabe brauchst:
Bisher hast Du nur Nachrichtenversand in einer Richtung gezeigt. So
besonders überzeugend ist das nicht, das haben viele Leute unter
Windows 3.0 auch schon hinbekommen. Spannend wird erst die
bidirektionale Kommunikation.
Schon mal versucht, mit diesem Ansatz das Problem mit den tafelnden
Philosophen zu lösen und zu beweisen, daß die Lösung korrekt ist?
[linux-kernel & linux-mm]
> Andi kennt diese Listen und liest die groessere davon. Es ist
> also genauso praktisch wie dein Zeugs und hat NICHTS mit
> Geruechten zu tun.
Daß die Kernel-Leute mit dem Konzept Probleme haben, spricht nicht
automatisch gegen das Konzept. Oder soll man jetzt nur noch stark
typisierte Sprachen (und mich meine: wirklich stark) verwenden, nur
weil diese Damen und Herren mit signed und unsigned nicht umgehen
können? Wenn man das konsequent verfolgen würde, wäre das für die
Softwarequalität vielleicht gar nicht mal so schlecht.
Im Userland hat man in der Regel sowieso ganz andere Probleme, und man
hat vor allem nicht mit so viel undokumentierten Schnittstellen und
historischem Balast zu kämpfen. Außerdem kann man vielleicht ein wenig
geeignetere Werkzeuge verwenden.
> Das der C-Compiler einen da wenigstens in Bezug auf atomare
> Operationen unterstuetzen koennte, ist allerdings wahr *seufz*
Die Helden von der Kernel-Front werden ja jedes Mal von einer neuen
GCC-Release überrollt, da der Optimierer wieder mal ein bißchen anders
arbeitet als bisher und die impliziten Annahmen nicht mehr zutreffen.
[GNU/Linux & Threads]
> mit welchen Mitteln (wenn überhaupt) Anforderungen des Standards
> umgesetzt werden, der wird alleine deshalb nie wieder Threads
> anfassen.
Threads aus dem Standard sind sowieso ziemlich böse: Sind nicht Dinge
wie euid und egid prozeßspezifische Attribute? Irgendwie ist die
Lösung mit dem LPSECURITYCONTEXT (oder wie das Dinges heißt) noch
brauchbarer.
>> Diese Probleme existieren auch, wenn man mehrere Prozesse verwendet
>> und die Interprozeßkommunikation über eine gemeinsam genutzte
>> Speicherregion abwickelt. Insofern ist das kein thread-spezifisches
>> Problem.
>
> shm ist aber unbequem genug das es sich jeder 2-3x ueberlegt bevor er es
> benutzt. Das ist eine gute Sache.
Und was benutzt man dann zum Datenaustausch? Pipes?
>> > Ich glaube du wirst dir ziemlich schwertun, ein Betriebssystem zu finden,
>> > das Prozesse, aber kein Equivalent zu einer Pipe hat.
>>
>> Es gibt aber viele Betriebssysteme, bei denen das Anlegen eines
>> Prozesses relativ teuer ist.
>
> Anlegen ist relativ uninteressant.
Bei den Unterschieden, die auf einigen Nachbauten auftauchen,
vielleicht nicht mehr.
Beim Anlegen kann man außerdem einmal relativ sicher dem neuen Thread
ein paar Daten auf den Weg geben. ;-)
> Ernsthafte Software arbeitet deswegen mit Threadpools wo nur ganz
> selten ein neuer erstellt wird, oder sie baut nur selten
> Verbindungen auf. Wie lange es dann dauert einen Thread oder Prozess
> zu erzeugen ist deswegen relativ uninteressant.
Naja, manchmal hat man kaum eine Chance, Thread-Pools zu bilden
bzw. muß dafür tonnenweise zusätzliche Schnittstellen erzeugen.
> Es gibt auch aber Operationen die mit Threads deutlich teurer sind als
> bei Prozessen. Alles was zB die Speichermappings aendert (mmap etc.)
> muss an alle CPUs die einen Thread vor dir laufen haben koennten einen
> InterProcessorInterrupt schicken um die TLBs zu loeschen. Das ist sehr
> langsam und kann ein Problem werden wenn du viel Speicher allozierst etc.
Hmm, interessanter Aspekt. Und das kann man nicht durch großzügiges
Mapping von Speicher erledigen?
Ich sagte, dass der normale C++ Programmierstil hilft, die Probleme zu
minimieren, ich habe nicht behauptet, dass er sie beseitigt. Es ist mir klar,
dass get/set Methoden fuer sich genommen nicht alle Probleme loesen (das waere
auch zu schoen:-). Im Gegensatz zum dem was in C oft gemacht wird (direkter
Zugriff auf globale Variablenn), ist es aber ein gewaltiger Fortschritt.
> Wer so etwas regelmäßig macht, erzeugt Code, der sich nie auf andere
> OO-Sprachen portieren läßt (außer Ada, welches noch am ehesten das
> Objektmodell von C++ implementiert). Ob man das will?
Ich habe bisher Code von einem OS auf ein anderes portiert und auch mal von
Pascal nach C, oder C nach C++ (was beides eher ein Neu-Schreiben war). Eine
Portierung von einer OO-Sprache in eine andere ist mir bisher nicht
untergekommen. Aus dem Grund bin ich in der Richtung wohl nicht sensibilisiert
genug, sprich: Nein, ich will den Code nicht auf eine andere OO Sprache
portieren.
> Ferner geht aus dem Codeabschnitt nicht hervor, wie Fehler in den Jobs
> behandelt werden, was passiert, wenn die Mailbox überläuft (ob sie das
> überhaupt tut) oder wenn RequestStop() zweimal aufgerufen wird.
Im bestehenden Beispiel gibt es keine Fehler in den Jobs (Du dachtest an
"Fatal Error in printf(): Console on fire" oder so?). Generell ist
Fehlerbehandlung immer etwas diffizil, das ist unabhaengig von Threads. Zur
Erinnerung: Es ging um thread-relevante Aspekte eines Programms, nicht um den
Netzwerkscode, nicht um Crypto-Algorithmen und auch nicht Fehlerbehandlung.
Zu Deinen anderen Anmerkungen: Die Mailbox laeuft nicht voll (Put() blockiert
dann), und RequestStop kann nicht zweimal aufgerufen werden, da ein
ShutdownJob definitiv die letzte Nachricht ist, die ein Thread abhandelt (es
setzt u.a. RunDown).
> Außerdem dürfte der Nachweis schwer werden, daß für ein gegebenes
> Problem die Jobs fair verteilt werden (insbesondere wenn die Jobs
> selbst wieder neue Jobs generieren).
Fuer das gegebene Problem ist es voellig egal, ob die Nachrichten fair
verteilt werden. Wenn wenig Nachrichten einlaufen, und das Betriebssystem die
Nachrichten immer demselben Thread bzw. derselben CPU zuteilt, wenn der frei
ist - umso besser.
> In einer typischen Anwendung, bei der Datenfluß nicht ein Sturzbach in
> eine Richtung ist, wirst Du Muster auf noch höherer Ebene
> zusammenstricken müssen.
Das weiss ich natuerlich. Das vom OP dargestellte Problem ist aber ein simples
Producer/Consumer Problem, und dafuer ist der dargestellte Datenfluss korrekt.
Die Aussage mehrerer Leute war, dass eine Loesung dafuer mit Threads viel zu
kompliziert und fehlertraechtig sei. Das wollte ich widerlegen, nichts weiter.
> Bisher hast Du nur Nachrichtenversand in einer Richtung gezeigt. So
> besonders überzeugend ist das nicht, das haben viele Leute unter
> Windows 3.0 auch schon hinbekommen. Spannend wird erst die
> bidirektionale Kommunikation.
Zwei Mailboxen reicht normalerweise aus, evtl. kombiniert mit einer festen
Anzahl Nachichten. Wenn Du den Mailboxen die Groesse 1 gibst, dann hast Du das
klassische synchrone Message Passing (send() ist asynchron, receive() blockt).
Es kommt natuerlich etwas auf das jeweilige Problem an. Du darfst mir aber
insofern vertrauen, als dass die meisten meiner Programme eine Kommunikation
in zwei Richtungen beinhalten, und dass es nicht wesentlich schwieriger ist.
Wobei das Problem um das es ging eine zweiseitige Kommunikation nicht
benoetigt, zumindest soweit ich es verstanden habe.
> Schon mal versucht, mit diesem Ansatz das Problem mit den tafelnden
> Philosophen zu lösen und zu beweisen, daß die Lösung korrekt ist?
Es gibt genuegend Dokumente im Web, die sich damit befassen, das Dining
Philosophers Problem mit Hilfe von Message Passing zu loesen. Ich glaube
deshalb nicht, dass es jemandem hilft, wenn ich das auch noch durchkaue. Das
betrifft im uebrigen auch mehrere Deiner uebrigen Punkte: So interessant diese
auch sind, mir ging es darum, die Aussage, Threads seien zu kompliziert und
fehlertraechtig, zumindest teilweise zu widerlegen. Und zu dieser Widerlegung
gehoert es (zumindest in meinen Augen) nicht, Standardloesungen fuer
Standardproblem nochmal zu veranschaulichen.
Es gibt in diesem (News-) Thread Leute, die versuchen, durch Rumpoebeleien zu
"ueberzeugen". Ich haette gerne, dass jemand, der unvoreingenommen ist, meine
Behauptungen weitgehend nachvollziehen kann, und versteht, warum ich so
argumentiere. Das setzt aber voraus, dass ich primaer daran arbeite, die von
mir bisher gemachten Behauptungen zu belegen, und nicht, neue Fragestellungen
zu finden, so interessant diese auch sein moegen. Ich bin gerne bereit, auch
Fragestellungen a la "Wie loest man das Dining Philosophers Problem mit
Message Passing?" mit Dir zu diskutieren. Nur hat das mit meiner
Ausgangsposition "Threads sind beherrschbar wenn man mit vernuenftigen
Werkzeugen und einem klaren Kopf rangeht" nichts mehr zu tun.
Ja, natürlich.
> > Anlegen ist relativ uninteressant.
> Bei den Unterschieden, die auf einigen Nachbauten auftauchen,
> vielleicht nicht mehr.
> Beim Anlegen kann man außerdem einmal relativ sicher dem neuen Thread
> ein paar Daten auf den Weg geben. ;-)
Vor allem finde ich die Argumentation wirklich grotesk! Erst werden
Threads empfohlen, weil sie schneller erzeugbar sind als Prozesse, und
dann kommt im nächsten Halbsatz, daß man aber Thread-Pools nimmt, weil
das zu langsam ist. Damit ist doch die ganze Daseisberechtigung für
Threads dahin! Dann kann man auch gleich Prozesspools nehmen!
> > Ernsthafte Software arbeitet deswegen mit Threadpools wo nur ganz
> > selten ein neuer erstellt wird, oder sie baut nur selten
> > Verbindungen auf. Wie lange es dann dauert einen Thread oder Prozess
> > zu erzeugen ist deswegen relativ uninteressant.
> Naja, manchmal hat man kaum eine Chance, Thread-Pools zu bilden
> bzw. muß dafür tonnenweise zusätzliche Schnittstellen erzeugen.
Hey, aber dafür hat er doch seine grandiose Wrapper-Bibliothek, die das
alles für ihn macht!
Felix
>> 1. Der Code enthaelt nur High-Level Konstrukte. Semaphoren usw. sind
>> nicht notwendig.
>
> Semaphoren sind nicht High Level.
> Aber das sagte ich ja bereits. Wahrscheinlich ist das zu hoch für dich.
> Und neues Wissen braucht jemand mit deiner Perfektion ja nicht.
dein Wissen möchte ich garnicht angreifen (nur damit du mich nicht falsch
verstehst), aber vielleicht solltest du dir den oben gequoteten Text
nochmal durchlesen.
MFG
Niklas
--
http://www.nhofmann.de/
http://www.xxx-area.net/
mailto:niklas...@web.de
Du hast Recht, ich wollte schreiben, daß Semaphoren nicht Low Level
sind, und _das_ hatte ich auch vorher schon geschrieben ;)
> Und wie bei allen Dingen die gefaehrlich sind, beeinflusst
> derjenige, der sich der Gefahr aussetzt in einem sehr grossen
> Masse, ob irgendwas nur gefaehrlich aussieht, oder ob es
> wirklich gefaehrlich ist.
Dann bleibe bitte von Atomkraftwerken fern. Eine Gefahr wird
nicht automatisch abgewendet, indem sich jemand der um die
Gefahren weiss. Derjenige weiss nur Gefahren (= Threads in diesem
Thread) zu meiden oder bei akutem Eintreten diese zu beseitigen.
>> Wenn du solche Minimierung von Gefahren nicht benoetigst, weil du
>> so ein genialer Programmierer bist, das dir nie, aber auch
>> wirklich nie Fehler unterlaufen, nie races eingebaut werden und
>> alle Programmierer von allen Libraries, die du so verwendest
>> genauso fehlerfrei arbeiten, dann und NUR dann geb ich dir recht.
> Wer ohne Vorbereitung den Salto Mortale wagt ist einfach nur
> dumm, wer sich dagegen gut vorbereitet, fuer den ist die damit
> verbundene Gefahr absolut kalkulierbar und er kann problemlos
> damit seinen Lebensunterhalt damit verdienen.
Ja, wenn man auf dem Jahrmarkt arbeitet. Normale Menschen bewegen
sich einfach ohne Salto Mortale, erregen weniger Aufsehen und
deren Code laeuft dann auch mit weniger Problemen.
> Neben der Verwendung von High Level Konstrukten (wo moeglich) gibt es noch
> mehr Sachen, die einem bei Threads das Leben erleichtern. Eine davon ist die
> Kombination mit einer der gaengigen OO Sprachen (bei mir ist das C++).
> Interessanterweise haben gute C++ Programme naemlich Eigenschaften, die die
> Gefahren von Threads minimieren:
> * Wenn Threads als Objekte dargestellt werden, dann sind private Daten einer
> Instanz automatisch nicht von aussen zugaenglich (keine
> Reentranzprobleme).
> * Wenn ein Thread ein Objekt ist, dann existieren diese Daten jeweils
> automatisch per Thread und sind damit wesentlich ungefaehrlicher als
> globale Variablen (die in C++ sowieso verpoent sind - ein weiterer
> Vorteil).
> * Wer get/set Methoden zum Zugriff auf Instanzdaten verwendet, der hat
> automatisch bereits die Schnittstellen geschaffen, an denen man notfalls
> mit Mutexen Resource Locking betreiben muss.
Wer Message Passing zwischen Prozessen macht, hat die
Schnittstellen bereits da INKLUSIVE Locking.
> * Mutex Locking laesst sich in ein Objekt verpacken, damit sorgt der
> Compiler automatisch dafuer, dass Mutexe beim Verlassen eines Blocks
> wieder entriegelt werden:
> class ResourceLock {
> Mutex& M;
> public:
> ResourceLock (Mutex& aM): M (aM) { M.Lock(); }
> ~ResourceLock () { M.Unlock(); }
> }
Und wenn du die ueber mehrere Bloecke und evtl. noch ein paar
Callbacks hindurch freigeben musst? Locks haben nur in einer
perfekten Welt Blockstruktur.
Sobald man aber sehr viele Synchronisationspunkte und Locks hat
und diese zusammenfasst (performance!), steht man vor recht
seltsamen Plaetzen fuer lock und unlock.
> Um das, was ich sage mal zu demonstrieren habe ich 50 Zeilen Code
> zusammengehackt.
Danke, dass du uns Message Passing durch eine Pipe gezeigt hast.
Nichts anderes war mein Vorschlag. Du hast nur den
Thread-Overhead hinzugefuegt (spezielle Libraries, spezielle
Varianten von libC-Funktionen usw. usf.).
> * Der Code ist automatisch portabel. Alle verwendeten Konstrukte existieren
> in identischer Form fuer alle gaengigen Betriebssysteme (wobei "gaengig"
> hier natuerlich meine Version von "gaengig" ist).
Die Definition diese Gruppe ist "Unix". Somit haette ein simples:
int main(void) {
pid_t child;
int fds[2];
if (pipe(&fds) == -1)
return EXIT_FAILURE;
if ((child = fork()) != -1) {
if (!child) {
char buf[PIPE_BUF]="Hello Child\n";
close(fds[0]);
write(fds[1], buf, sizeof buf);
return !(waitpid(child, NULL, 0)!=-1);
} else {
char buf[PIPE_BUF];
close(fds[1]);
read(fds[0], buf, sizeof buf);
printf("%sHello Daddy\n",buf);
exit(0);
}
} else {
return EXIT_FAILURE;
}
return 0;
}
gereicht. Auf die Fehlerbehandlung und das Testen hab ich ebenso groszzuegig
verzichtet, wie du. Mit all dem waeren es sicher auch knapp 50
Zeilen geworden.
> * Es ist absolut einfach, mehrere Arbeiter-Threads zu erzeugen, und den Code
> damit skalierbar zu machen: Mehrfach "new MyThread" und das war's (es muss
> zum Beenden natuerlich die entsprechende Anzahl Shutdown Nachrichten in
> der Mailbox abgelegt werden).
Ja, forken geht aehnlich einfach. Wenn du das child noch jeweils
in eine einzelnen Funktion verpackst, dann sieht es auch schon
fast so aus, wie deine Threads.
> Wenn Du Dir den Code mal anschaust, dann wirst Du vermutlich ein paar Sachen
> merken:
> 1. Der Code enthaelt nur High-Level Konstrukte. Semaphoren usw. sind nicht
> notwendig.
Meiner enthaelt sowas auch nicht. Deswegen nehme ich ja pipes. Da
passiert sowas implizit und 'zigmal effizienter im Kernel.
Die Anzahl der benoetigten Kontextwechsel ist identisch.
> So, und jetzt bist Du wieder dran: Wo genau lauern Deiner Meinung nach im
> Beispielcode die Gefahren der Thread-Programmierung?
In der Library, die das kapselt, dem expliziten Locking,
aliasing-Situationen uvam. Bei mir kuemmert sich der Kernel drum,
bei dir musst du es selber oder in der Library machen und hoffen,
dass weder du, noch die etwas uebersehen haben. Einem
Kernel traue ich sowas eher zu, als einer Library.
Der Rest wurde schon diskutiert.
Grusz
Ingo
Wenn ich das muss, dann muss ich mich fragen, ob ich nicht was falsch gemacht
habe.
Ist sowas denn Deiner Meinung nach einfacher, wenn ich separate Prozesse
verwende? Bei Prozessen sprichst Du Dich fuer einfache Loesungen aus (Message
Passing mit Pipes), aber bei Threads versuchst Du alles, moeglichst
kompliziert zu reden, um daran zu zeigen, wie schrecklich Threads sind.
> Sobald man aber sehr viele Synchronisationspunkte und Locks hat
> und diese zusammenfasst (performance!), steht man vor recht
> seltsamen Plaetzen fuer lock und unlock.
Wie kommt es, dass Du bei Deinen Pipes auf all diese Dinge verzichten kannst,
bei Threads aber denkst, dass man sie unbedingt braucht? Es gibt kein
sachliches Argument, was diese Annahme stuetzt. Threads sind eigentlich auch
nichts anderes Prozesse, bei denen bestimmte Resourcen geshared werden. Alle
Konstrukte, die mit Prozessen funktionieren, funktionieren immer auch mit
Threads. Wegen den fehlenden Resource-Sharing gilt das aber nicht umgekehrt.
> Danke, dass du uns Message Passing durch eine Pipe gezeigt hast.
> Nichts anderes war mein Vorschlag. Du hast nur den
> Thread-Overhead hinzugefuegt (spezielle Libraries, spezielle
> Varianten von libC-Funktionen usw. usf.).
Nein. Meine Loesung hat zwei Vorteile gegenueber Deiner:
* Ich kann nach Belieben mehr Threads starten, damit skaliert das Programm
auf SMP Maschinen ausgezeichnet. Einkommende Nachrichten werden
automatisch dem naechsten freien Thread zugeordnet, ohne dass ich etwas
tun muss. Das geht mit Pipes nicht ohne groesseren Aufwand.
* Bei meiner Loesung werden die Daten einmal vom Socket in einen Puffer
eingelesen und bleiben dann da, weil ich nur Zeiger auf diese Daten
weitergebe. Bei Pipes wird kopiert (auch wenn sich der Kernel Muehe gibt,
das moeglichst zu vermeiden - aber ganz geht es eben nicht). Deshalb ist
meine Loesung performanter.
>> * Der Code ist automatisch portabel. Alle verwendeten Konstrukte existieren
>> in identischer Form fuer alle gaengigen Betriebssysteme (wobei "gaengig"
>> hier natuerlich meine Version von "gaengig" ist).
>
> Die Definition diese Gruppe ist "Unix". Somit haette ein simples:
Das ist eine Unix Gruppe, aber muss ich deshalb die Tatsache, dass mein Code
auch auf anderen Betriebssysteme laeuft verschweigen? Wenn ich Lauffaehigkeit
auf anderen Plattformen praktisch geschenkt bekommen, dann ist fuer einige
Leute (unter anderem fuer mich) ein Vorteil. Anderen ist es egal, aber ich
kann zumindest nicht erkennen, dass es ein Nachteil sein soll.
> gereicht. Auf die Fehlerbehandlung und das Testen hab ich ebenso groszzuegig
> verzichtet, wie du. Mit all dem waeren es sicher auch knapp 50
> Zeilen geworden.
Ok. Jetzt mach mal zwei Worker draus mit automatischem Load-Balancing.
> Meiner enthaelt sowas auch nicht. Deswegen nehme ich ja pipes. Da
> passiert sowas implizit und 'zigmal effizienter im Kernel.
> Die Anzahl der benoetigten Kontextwechsel ist identisch.
Fuer die Anzahl der Kontextwechsel hast Du recht - Gleichstand was das angeht.
Deine Loesung kopiert aber die Daten mehrfach, meine nicht.
> In der Library, die das kapselt, dem expliziten Locking,
> aliasing-Situationen uvam. Bei mir kuemmert sich der Kernel drum,
> bei dir musst du es selber oder in der Library machen und hoffen,
> dass weder du, noch die etwas uebersehen haben. Einem
> Kernel traue ich sowas eher zu, als einer Library.
Der von mir angefuehrte Code enthaelt kein explizites Locking und keine
Aliasing Situationen. Was bleibt ist also letzten Endes die Vermutung, ich
koennte evtl. Fehler im Library Code gemacht haben, d.h. das Argument ist
eigentlich kein technisches. Darueber laesst sich natuerlich schwer
diskutieren.
"Es gibt keine gefährlichen Dinge, es gibt nur gefährliche Menschen".
Ansonsten ist 'Was der Bauer nicht kennt, daß frißt er nicht' als
Argumentation weder neu noch originell und speziell auf dieses Thema
bereits bis zum Erbrechen angewendet worden.
Das es bei Leuten üblicherweise aushakt, sobald mehr als zwei
unabhängige Faktoren beteiligt sind, ist auch nicht neues.
Faustregel: Wem Threads 'gefährlich' vorkommen, der soll bitte die
Finger von Prozessen lassen, weil ihm noch nicht einmal
bewußt ist, daß er über exkt dieselben Problem stolpern
wird, nur in etwas aufwendigerem Rahmen.
'tschuldigung für dieses Posting ohne Inhalt, aber allmählich reichts
mal.
F'up2 poster
--
stone me
> Ansonsten ist 'Was der Bauer nicht kennt, daß frißt er nicht' als
> Argumentation weder neu noch originell und speziell auf dieses Thema
> bereits bis zum Erbrechen angewendet worden.
> Das es bei Leuten üblicherweise aushakt, sobald mehr als zwei
> unabhängige Faktoren beteiligt sind, ist auch nicht neues.
> Faustregel: Wem Threads 'gefährlich' vorkommen, der soll bitte die
> Finger von Prozessen lassen, weil ihm noch nicht einmal
> bewußt ist, daß er über exkt dieselben Problem stolpern
> wird, nur in etwas aufwendigerem Rahmen.
> 'tschuldigung für dieses Posting ohne Inhalt, aber allmählich reichts
> mal.
Das war nicht ohne Inhalt. Es hatte Inhalt, und zwar schädlichen.
Rainer, bitte schreibe keine Programme, die du weitergibst!
Deiner Argumentation folgend kann man auch verteilte Echtzeitdatenbanken
in Java unter Windows CE auf Smartcards implementieren, unter Verwendung
von CORBA, mit einem cryptographischen Agentensystem in VBScript machen.
Programmieren kann jeder. Gute Programme schreiben kann fast keiner.
Die, die es können, erkennt man daran, daß sie in Produktivumgebungen
keine nicht wohlverstandenen Technologien einsetzen. Sie folgen keinen
Modeerscheinungen und Marketing-Trends von Sun und Microsoft. Schau dir
nur mal an, wie viele großartige Projekte in Java angekündigt wurden,
udn aus wie vielen davon etwas geworden ist! Klar, es gibt Leute, die
können in Java performante, zuverlässige Software entwickeln. Man kann
auch in VBScript eine Office-Suite schreiben. Das macht es nicht zu
einer guten Idee!
Threads _sind_ eine sehr schlechte Idee. Sie bringen keine Vorteile,
öffnen deine Programme aber einem Haufen neuer potentieller Probleme.
Die Benutzung von Threads ist ein gutes Zeichen dafür, daß derjenige
keine Ahnung hat und/oder beweisen wollte, daß es doch geht. Und in den
meisten Fällen geht es eben doch nicht. Ausnahmen bestätigen die Regel.
> F'up2 poster
ignored.
> --
> stone me
Ich glaube, du bist schon stoned.
> * Ich kann nach Belieben mehr Threads starten, damit skaliert das Programm
> auf SMP Maschinen ausgezeichnet.
Threads skalieren schlechter als Prozesse auf SMP-Systemen.
Bei Prozessen kriegt jeder Prozess schreibbare Seiten für sich alleine
(außer natürlich du hebelst das durch shared memory aus, was nicht zu
empfehlen ist). Daraus folgt, daß die anderen Prozessoren nicht ständig
in ihrem Cache heruminvalidieren müssen.
Auf manchen Plattformen (sparc comes to mind) gibt es keine
Cache-Kohärenz in Hardware, so daß shared memory nur geht, indem man
entweder Semaphoren des Systems benutzt oder in dem Bereich den Cache
ganz abschaltet. Performance, nein danke.
Aber das wußtest du ja sicher schon alles und hast jetzt ein prima
Gegenargument in der Tasche, wie ich dich kenne...?
> * Bei meiner Loesung werden die Daten einmal vom Socket in einen Puffer
> eingelesen und bleiben dann da, weil ich nur Zeiger auf diese Daten
> weitergebe. Bei Pipes wird kopiert (auch wenn sich der Kernel Muehe gibt,
> das moeglichst zu vermeiden - aber ganz geht es eben nicht). Deshalb ist
> meine Loesung performanter.
Das Gegenteil ist der Fall.
Aber das wußtest du ja sicher schon alles und hast deine Inkompetenz nur
vorgetäuscht...?
> > Die Definition diese Gruppe ist "Unix". Somit haette ein simples:
> Das ist eine Unix Gruppe, aber muss ich deshalb die Tatsache, dass mein Code
> auch auf anderen Betriebssysteme laeuft verschweigen?
Die Konzepte, die dein Code umsetzt, sind schlecht.
Niemanden interessiert, wo schlechte Software läuft und wo nicht.
> Wenn ich Lauffaehigkeit
> auf anderen Plattformen praktisch geschenkt bekommen, dann ist fuer einige
> Leute (unter anderem fuer mich) ein Vorteil.
Das ist eine Lüge!
Alles, was du geschenkt bekommst, ist daß der _Thread_-Teil portabel ist
(ist er nicht, aber deine Wrapper-Library kapselt das). Der Rest ist
genau so unportabel wie vorher. Leute, die keine Threads benutzen, sind
also per Definition sogar _portabler_ als du, weil sie die Layer über
den Thread-Untiefen nicht brauchen.
Felix
[...]
> Deiner Argumentation folgend kann man auch verteilte Echtzeitdatenbanken
> in Java unter Windows CE auf Smartcards implementieren, unter Verwendung
> von CORBA, mit einem cryptographischen Agentensystem in VBScript
> machen.
Sicher kann man. Es wird außerdem n Situationen geben, wo es sogar gar
nicht anders geht. Obwohl ich mir im Moment gerade keine vorstellen mag.
> Programmieren kann jeder.
... irgendwie lernen.
Obwohl mir da angesichts der jüngsten sendmail-Katastrophen allmählich
gewisse Zweifel kommen [<stoßseufzer>Wieviele Jahr braucht manche
Leute, um endlich mal zu kapieren, daß Programme, die nicht mit
beliebigen Eingabedaten zurechtkommen, _kaputt_ sind?</>] ...
> Schau dir nur mal an, wie viele großartige Projekte in Java
> angekündigt wurden, und aus wie vielen davon etwas geworden ist!
Java ist C++ für Arme. Sowas schaue ich mir nicht an. Außer, ich werde
dafür bezahlt ...
> Threads _sind_ eine sehr schlechte Idee. Sie bringen keine
> Vorteile,
Sie bringen indem Moment Vorteile, wo ich 'grundsätzlich' parallelle
Operationen auf gemeinsamen Datenstrukturen habe. Also beispielsweise,
wenn ich eine Liste von irgendwassen habe, die in Abhängigkeit von
Eingaben (vom Netz) gefüllt wird, wo nach Ablauf eines Timeouts
irgendwelche Operationen angestoßen werden sollen. Thread 1 liest Daten
und legt frisch konstruierte Objekte in einer Queue ab, Thread 2
schläft vor sich hin und wacht gelegentlich auf, überprüft timeouts,
sammelt abgelaufene Objekte und führt ggf weitere Aktionen durch.
Das wird multithreaded ziemlich straight und einigermaßen haarig,
sobald ich Eingaben und Verarbeitung im selben thread of execution
machen möchte: Dann darf ich nämlich 'irgendeine Form von Scheduling'
für diese zwei Aufgaben von Hand implementieren, während ich das
andernfalls 'jemand anderem' überlasse, der das sowieso macht (zB dem
Kernel). Ggf bekomme ich sogar noch unterschiedliche Prioritäten (zB,
daß der Eingabe-Thread immer gegenüber dem anderen bevorzugt wird,
solange er etwas zu tun hat und nicht freiwillig blockiert) fast
gratis dazu.
Wie der Zufall so spielt, fallen mir auf Anhieb zwei Programme ein,
die genau das brauchen. Während das erste eher ein imaginäre
Spielzeugprojekt ist, für das mir ohnehin die Zeit fehlt (einen
simplen DNS-Cache schreiben, den ich ohne innere Blutungen verwenden
kann und den als PD zur Verfügung zu stellen), sitzt das zweite real
hier rum verursacht allgemeinen Netzterror, der sich obendrein in vier
bis acht Monaten zu katastrophalen Ausmaßen potenzieren wird. Weil das
leider mit NetBSD zurechtkommen muß, sind Threads keine Option :-(...
> Ich glaube, du bist schon stoned.
Das auch.
--
stone me
Während man mit g/thread/s/thread/C, g/prozeß/s/prozeß/ada dem kompletten
Thread hier ein sehr bekanntes Gesicht verleihen kann. Es gibt
Threads, also werden sie verwendet. Falls jemand es schafft, damit ein
Problem von ihm zu lösen, soll er doch. Falls nicht, wen kümmerts?
> Programmieren kann jeder.
... außer Eric Allmann ... (SCNR)
> Threads _sind_ eine sehr schlechte Idee.
Schön. Falls sich also jemand gerne welche aufladen möchte, dann laß
ihn doch einfach. Tendenziell wird er/sie dann nicht funktionierenden
Schrott produzieren, der im Laufe der Zeit von alleine austirbt.
Problem gelöst.
> Die Benutzung von Threads ist ein gutes Zeichen dafür, daß derjenige
> keine Ahnung hat und/oder beweisen wollte, daß es doch geht.
2/3 der 'fortgeschrittenen' Konstruktionen in irgendwelchen Programmen
stehen vermutlich bloß da, weil jemand mal 'etwas cooles' machen
wollte. Erstaunlicherweise geht das häufig sogar einigermaßen gut.
Der Versuch, sowas auszurotten, ist sicher lobenswert, aber ich halte
ihn für vergeblich.
> Ich glaube, du bist schon stoned.
Das auch.
--
stone me
> Wenn ich das muss, dann muss ich mich fragen, ob ich nicht was
> falsch gemacht habe.
Ja, du hast versucht deine 5000+ Locks aus der ersten Designstufe
zu verringern.
> Ist sowas denn Deiner Meinung nach einfacher, wenn ich separate Prozesse
> verwende?
Es gibt weniger potentielle Synchronisiationspunkte. Jeder dieser
Punkte wird explizit festgelegt. Vergisst man einen, wird nicht
kommuniziert. Bei Threads wird bei einem vergessenen
Synchronisationspunkt nur gelegentlich mal falsch kommuniziert
und das ruft dann sporadische Fehler hervor. Da fand ich halt
Prozesse einfacher.
> Bei Prozessen sprichst Du Dich fuer einfache Loesungen aus (Message
> Passing mit Pipes), aber bei Threads versuchst Du alles, moeglichst
> kompliziert zu reden, um daran zu zeigen, wie schrecklich Threads sind.
Sagen wir so: Es gehen mehr Schweinereien, also werden die auch
gemacht. Aus dem gleichen Grund argumentieren auch viele Leute
gegen C und das teilweise zu recht.
>> Sobald man aber sehr viele Synchronisationspunkte und Locks hat
>> und diese zusammenfasst (performance!), steht man vor recht
>> seltsamen Plaetzen fuer lock und unlock.
> Wie kommt es, dass Du bei Deinen Pipes auf all diese Dinge verzichten kannst,
> bei Threads aber denkst, dass man sie unbedingt braucht?
Weil du Pipes nicht verstanden hast?
Da uebernimmt das ganze gelocke das OS und read/write wird eine
atomare Operation, wenn ich mit einem Puffer der Groesze PIPE_BUF
arbeite.
Ich kann mir da jetzt beliebig viele Producer und Consumer
abforken, wenn die Reihenfolge der Messages egal ist. Wenn die
Reihenfolge nicht egal ist, kann ich dann wieder zur
SysV-Msg-Queue zurueckgreifen und halt die Msg-Ids clever
vergeben.
> Es gibt kein
> sachliches Argument, was diese Annahme stuetzt. Threads sind eigentlich auch
> nichts anderes Prozesse, bei denen bestimmte Resourcen geshared werden. Alle
> Konstrukte, die mit Prozessen funktionieren, funktionieren immer auch mit
> Threads. Wegen den fehlenden Resource-Sharing gilt das aber nicht umgekehrt.
Richtig, es gehen Dummheiten wie Thread1->Value++ und
Thread2->Value++ und stehen sicher irgendwo in jedem Code der
Threads verwendet ohne die noetigen Schutzmechanismen. Ausserdem
benoetigen diese Programme oft mehr Synchronisation, wenn es
sauber sein will, oder man landet halt bei Konstrukten wie den
von mir beschriebenen. Beides sehe ich eher als Nachteil.
Threads haben ihren Platz in Systemen, die sowieso keinen
Speicherschutz bieten und eine ueberschaubare gesamte
Code-Groesze haben (Echtzeit-Systeme, groeszere Mikrocontroller
usw.) oder halt in Systemkernen, wo es sowieso nur eine VM gibt.
> * Ich kann nach Belieben mehr Threads starten, damit skaliert das Programm
> auf SMP Maschinen ausgezeichnet. Einkommende Nachrichten werden
> automatisch dem naechsten freien Thread zugeordnet, ohne dass ich etwas
> tun muss. Das geht mit Pipes nicht ohne groesseren Aufwand.
Wie bitte? Ich muss nur einmal mehr forken und die jeweils
richtige Seite der Pipe schlieszen um entweder einen neuen
producer, oder einen neuen consumer hinzuzufuegen.
Das Load-Balancing uebernimmt dabei der Kernel selbst, weil der
sowieso mehr von der Gesamtload weisz, als irgendein popeliges
Userspace-Programm.
Ausserdem werden durch die Kopie die jeweiligen L1-Caches der
CPUs entkoppelt. Threadbasierte Programme spielen gerne
Cache-Pingpong. Die Theoretiker vergessen solche Sachen gerne bei
ihren Algorithmen.
Es gab zur Einfuehrung des 64-Bit Alphas damals sogar ein nettes
Paper dazu, dass eine Optimierung des Speicherlayouts bei
heutigen CPUs weit mehr bringt, als eine Optimierung auf linearen
Taktzyklen.
> * Bei meiner Loesung werden die Daten einmal vom Socket in einen Puffer
> eingelesen und bleiben dann da, weil ich nur Zeiger auf diese Daten
> weitergebe. Bei Pipes wird kopiert (auch wenn sich der Kernel Muehe gibt,
> das moeglichst zu vermeiden - aber ganz geht es eben nicht). Deshalb ist
> meine Loesung performanter.
Wenn du deine CPUs immer mit abgeschalteten Caches laufen laesst,
dann bestimmt. Der Speicher ist der Engpass, nicht die
Ausfuehrungseinheiten. Ab ca. 8 Prozessoren kostet dich ein Lock
mehr Zeit, als Daten in per-CPU-Pools vorzuhalten.
Bei meiner Loesung werden die Daten nur zwischen zwei Caches
kopiert. Auch bei 32 CPUs und 31 Consumern. Bei deiner Loesung
wird nicht kopiert, aber 31 CPUs muessen die entsprechenden
Cachelines neu laden.
Da halte ich meine fuer performanter und skalierbarer und fange
noch nichtmal an, von NUMA zu reden ;-)
Das meinte Fefe mit seinem simplen "Das Gegenteil ist der Fall".
>> gereicht. Auf die Fehlerbehandlung und das Testen hab ich ebenso groszzuegig
>> verzichtet, wie du. Mit all dem waeren es sicher auch knapp 50
>> Zeilen geworden.
> Ok. Jetzt mach mal zwei Worker draus mit automatischem Load-Balancing.
Fuege bitte mal fuer mich noch so ein "if (child2=fork()!=-1)"-Konstrukt
in das erste ein, bevor du close(fd[1]) machst. Danke, damit hast
du einen weiteren Worker und automatisches Load-Balancing nach
Kriterien des Gesamtsystems. Warum, steht weiter oben in diesem
Posting.
> Deine Loesung kopiert aber die Daten mehrfach, meine nicht.
Warum das nicht von Nachteil auf SMP und NUMA ist, hab ich oben
ausgefuehrt.
> Der von mir angefuehrte Code enthaelt kein explizites Locking und keine
> Aliasing Situationen.
Das war ja auch ein simples Beispiel, kein realer Code eines
richtigen Programmes. Da findest du dann sowas. Oder auch nicht
und dein Kunde findet dann seltsames Verhalten, das du evtl.
absolut nicht nachvollziehen kannst.
Threads sind aber fuer eine Sache wirklich sehr gut: Fuer die
Erkennung der Parallelisierungsmoeglichkeiten innerhalb eines
Programmes. Fuer Entwuerfe also. Die Parallelisierung damit dann
gleich vorzunehmen liegt zwar nahe, nur ergibt der Entwurf selten
ein optimales Design.
MfG
Ingo Oeser
ich moechte mich aus der mittlerweile endlosen Diskussion aus
Threads vs. Process lieber raushalten. Allerdings interessiert
mich die obenstehende Bemerkung als Nutzer von Threads durchaus.
Koennte sich jemand mal dazu herablassen und mir die Cache-Problematik
etwas genauer erklaeren? Insbesondere, ob das auch fuer Einprozessor
Rechner
eine Rolle spielt (kann ich mir nicht vorstellen) und wie genau
Speicherzugriffe auf Mehrprozessorsystemen aussehen muessen,
damit dieses Cache-Pingpong auftritt (so auf die Schnelle hab
ich Schwierigkeiten mir was praxisrelevantes vorzustellen,
bei dem mehrere CPUs ständig die Daten aus der gleichen Cacheline
verwuerfeln).
Gruss,
Arne
> Auf manchen Plattformen (sparc comes to mind) gibt es keine
> Cache-Kohärenz in Hardware, so daß shared memory nur geht, indem man
> entweder Semaphoren des Systems benutzt oder in dem Bereich den Cache
> ganz abschaltet. Performance, nein danke.
Das stimmt aber nicht. Die Sun Rechner sind alle cache coherent.
Was sie nur nicht garantieren ist das andere CPUs Schreiboperationen in
genau der gleichen Reihenfolge sehen wenn du keine expliziten Memory Barriers
einbaust.
Es gibt zwar Speicherinterfaces, die nicht Cachecoherent sind (manche der
Clusterlinks), aber die werden normal nur mit speziellen Funktionsaufrufen
und nicht normalen Threads eingestzt.
-Andi
Hier mal das Paper:
http://www.mostang.com/~davidm/papers/expo97/paper/
> Allerdings interessiert mich die obenstehende Bemerkung als
> Nutzer von Threads durchaus. Koennte sich jemand mal dazu
> herablassen und mir die Cache-Problematik etwas genauer
> erklaeren? Insbesondere, ob das auch fuer Einprozessor Rechner
> eine Rolle spielt (kann ich mir nicht vorstellen) und wie genau
> Speicherzugriffe auf Mehrprozessorsystemen aussehen muessen,
> damit dieses Cache-Pingpong auftritt (so auf die Schnelle hab
> ich Schwierigkeiten mir was praxisrelevantes vorzustellen, bei
> dem mehrere CPUs staendig die Daten aus der gleichen Cacheline
> verwuerfeln).
Bei Einprozessorsystemen spielt das erst wieder bei der
Verclusterung selbiger eine Rolle.
Bei SMP haben die CPUs fast immer eigene L1-Caches (weil sie sind
ja mal so verloetet worden) und teilweise eigene L2 oder
L3-Caches.
Je groeszer diese Caches sind, desto weniger
Speicherbusbandbreite wird benoetigt. Gecached wird in sog.
"cache lines". Die sind je nach Prozessor und Cache-System
verschieden grosz. Beim L1 eines Pentiums sind die 32 Bytes
grosz.
Schreibt ein Prozessor in so eine Cache Line, so muessen sich
alle Prozessoren diesen Wert in ihren L1-Cache holen (strong
write ordering beim Intel), wenn sie einen Wert aus dieser
Cachline benoetigen.
Selbst wenn die Threads voellig andere Variablen benutzen, so
spielen sie Cache-Pingpong, wenn diese Variablen zufaellig in der
selben Cacheline liegen (z.B. 2 sehr kleine Objekte wurden mit
"new" kurz hintereinander alloziert, dank Polymorphismus kann das
auftreten).
Es gibt da noch weit krassere Situationen mit dem L2-Cache oder
bei NUMA, wo das dann alles noch etwas "teurer" ist. (So teuer,
das man Kopien des Kernels und diverser RO-Bereiche auf die
Knoten verteilt)
Das kann aber jemand anderes mal weiterfuehren...
Grusz
Ingo
Warum programmierst Du dann in C? Laut Deiner Logik gehen mehr Schweinereien
in C, also werden die auch gemacht. Kann es sein, dass Du unterschiedliche
Massstaebe anwendest, je nachdem wie es Dir gerade passt? Wenn Du in C
programmierst, dann doch nur deshalb, weil Du die Probleme von C zu
beherrschen glaubst, und weil Du weisst, dass Du eben genau die Schweinereien
*nicht* machst, wenn es drauf ankommt. Warum kannst Du nicht akzeptieren, wenn
andere Leute das ueber Threads sagen?
> Da uebernimmt das ganze gelocke das OS und read/write wird eine
> atomare Operation, wenn ich mit einem Puffer der Groesze PIPE_BUF
> arbeite.
>
> Ich kann mir da jetzt beliebig viele Producer und Consumer
> abforken, wenn die Reihenfolge der Messages egal ist.
PIPE_BUF ist ueblicherweise eine MMU Page. Auf ix86 Architekturen sind das
4KB, auf anderen Architekturen mehr oder (seltener) auch weniger. Das
bedeutet:
1. Du musst immer eine volle Seite an Daten durch die Pipe schieben, auch
dann, wenn die "Nutzlast" nur 20 Bytes betraegt. Wenn der andere Prozess
wartet, dann werden diese Daten einmal kopiert (Producer -> Consumer),
wenn er nicht wartet, dann zweimal (Producer -> Kernel Buffer ->
Consumer).
Nimm Dein eigenes Programm als Beispiel: Tatsaechlich uebertragen willst
Du 12 Zeichen ("Hello Child\n"). Auf meinem Rechner kopierst Du aber 4KB
(best case) bzw. 8KB (worst case). Diese unsinnige Kopiererei verdraengt
Daten aus dem Cache, die spaeter durch teure Speicherzugriffe wieder
nachgeladen werden muessen. Insofern halte ich die von Dir propagierten
Cache Probleme bei Threads fuer bestenfalls theoretisch.
2. Der ganze Mechanismus funktioniert nicht mehr, wenn Du mehr als PIPE_BUF
Daten hast. Er ist also nicht universell verwendbar, und Du musst schnell
auf andere, wesentlich unbequemere Mechanismen (SHM o.ae.) ausweichen.
3. Mir ist nicht bekannt, ob es eine Mindestgroesse fuer PIPE_BUF gibt, im
Stevens ist jedoch angefuehrt, dass PIPE_BUF fuer BSD 4.3+ den Wert 512
hat. Eventuell liegt die garantierte Mindestgroesse darunter, spaetestens
aber, wenn Du mehr als 512 Bytes hast, die Du verschieben willst, dann
funktioniert Dein Mechanismus nicht mehr, bzw. ist zumindest nicht mehr
zwischen den verschiedenen Unixen portabel.
Alle diese Nachteile hat meine Loesung nicht.
> Richtig, es gehen Dummheiten wie Thread1->Value++ und
> Thread2->Value++ und stehen sicher irgendwo in jedem Code der
> Threads verwendet ohne die noetigen Schutzmechanismen.
Es ist schade, dass Dein Glaube Dir den Blick ueber den Tellerrand verwehrt.
Ich hatte bereits erklaert, dass eben genau das nicht moeglich ist, wenn Du
Threads als C++ Objekte implementierst, weil der Compiler diese Zugriffe
ueberhaupt nicht zulaesst. Davon rede ich ja die ganze Zeit: Mit vernuenftigen
Hilfsmitteln sind Threads durchaus beherrschbar.
> Ausserdem
> benoetigen diese Programme oft mehr Synchronisation, wenn es
> sauber sein will, oder man landet halt bei Konstrukten wie den
> von mir beschriebenen. Beides sehe ich eher als Nachteil.
Warum zeigst Du nicht einfach auf, warum Programme mit Threads mehr
Synchronisation benoetigen? Das ist bis jetzt nur eine Behauptung von Dir,
die durch nichts belegt wird, was bisher gesagt wurde.
Kernel-Threads sind im Prinzip Prozesse, die bestimmte Resourcen sharen. Das
bedeutet nichts anderes, als dass ich *alle* Moeglichkeiten, die Du bei
separaten Prozessen hast auch fuer Threads nutzen kann (umgekehrt ist das
nicht so). In Anbetracht dessen duerfte es unmoeglich sein, nachzuweisen, dass
man mehr Synchronisation fuer Threads _braucht_, wie Du es versuchst
hinzustellen.
Wuerdest Du mir stattdessen jetzt erzaehlen, dass Deine bisherigen Versuche
mit Threads mehr Synchronisationsmechanismen benoetigt haben, dann wuerde das
wesentlich glaubhafter klingen, und wir koennten uns darueber unterhalten,
warum das so war, und was man evtl. dagegen machen kann.
> Wie bitte? Ich muss nur einmal mehr forken und die jeweils
> richtige Seite der Pipe schlieszen um entweder einen neuen
> producer, oder einen neuen consumer hinzuzufuegen.
Das funktioniert aber nur in den oben beschriebenen, recht engen Grenzen und
ist deshalb in vielen Faellen ueberhaupt nicht verwendbar. Ausserdem wird je
nach Nachrichtengroesse eine irrsinnige Menge Muell durch die Gegend kopiert.
> Bei meiner Loesung werden die Daten nur zwischen zwei Caches
> kopiert. Auch bei 32 CPUs und 31 Consumern. Bei deiner Loesung
> wird nicht kopiert, aber 31 CPUs muessen die entsprechenden
> Cachelines neu laden.
Unter der Annahme, dass der Producer 1000 Nachrichten pro Sekunde generiert
(fuer ein 32-fach SMP System ist das eher wenig) kopiert Deine Loesung im
Gegensatz zu meiner auf gaengiger Hardware etwa 4-8 Megabyte(!) pro Sekunde
durch die Gegend. Je nach Nachrichtengrosse ist der Hauptteil davon Muell, in
Deinem Beispielprogramm zum Beispiel mehr als 99,7% der Daten. Dieser Muell
bewirkt, dass
* ... andere wichtige Daten aus den Caches verdraengt werden, und dass
* ... diese Daten spaeter durch teure Speicheroperation wieder nachgeladen
werden muessen.
Wenn Du Pech hast, dann wird beim Nachladen der wirklich wichtigen Daten in
den Cache, die durch Deinen Muell soeben verdraengt wurden, eben dieser Muell
sogar noch in den Speicher rausgeschrieben.
Und Du bist wirklich sicher, dass Du versuchen solltest, dass alles als
Vorteile zu verkaufen?
>> Der von mir angefuehrte Code enthaelt kein explizites Locking und keine
>> Aliasing Situationen.
>
> Das war ja auch ein simples Beispiel, kein realer Code eines
> richtigen Programmes.
Ich mache Message Passing, Du machst Message Passing. Warum soll die eine
Loesung dabei zusaetzliches Locking brauchen, und die andere nicht?
Aber wie waer's denn mit folgendem Vorschlag: Wir schreiben beide ein
Programm. Deines verwendet Prozesse und Pipes, meines verwendet Threads und
Mailboxen. Zielplattform ist Unix (mit POSIX Threads in meinem Fall).
Das Programm simuliert ein simples Producer/Consumer Modell, wie das, worueber
wir die ganze Zeit reden. Folgende Groessen sind per Kommandozeile einstellbar:
* Anzahl der Nachrichten, die der Producer erzeugt.
* Groesse der Nachrichten, die der Producer erzeugt (Vorschlag: 1 Byte
bis 64 KByte).
* Anzahl der Consumer (Vorschlag: 1 bis 32)
Nach Programmende wird jeweils die verbrauchte Real/User/Systemzeit
ausgegeben, und zwar ohne Beruecksichtigung der Initialisierung. Die Routinen
fuer die Generierung der Daten, sowie das "Verbrauchen" gleichen wir ab, damit
wir nur die Unterschiede der Kommunikation messen. Die Verbraucher fassen
selbstverstaendlich alle Nutzdaten an, damit sie (falls nicht im Cache) in den
Speicher geladen werden muessen.
Ich denke, dass ich das in einer halben Stunde hinbekomme. Mit Prozessen geht
das ja angeblich noch einfacher als mit Threads, Du solltest also eher weniger
Zeit benoetigen.
Wir lassen dann die Programme mit unterschiedlichen Parametern (ueber die wir
uns vorher einigen) auf diversen Plattformen laufen, und haben dann
tatsaechlich einen realistischen Vergleich zwischen den beiden
Implementierungen.
Ich habe die Moeglichkeit, die Programme auf folgenden Plattformen zu
"vermessen":
* Single CPU ix86
* 2-fach SMP ix86
* 2-fach SMP RS/6000
* 4-fach SMP RS/6000
* 8-fach SMP RS/6000
Single CPU Sparc und MIPS Rechner stehen hier auch noch rum. Die sind zwar
schon aelter, kann ich evtl. aber auch nochmal anschmeissen. Wenn ich Dich
richtig verstanden habe, dann hast Du Zugriff auf ein 32-fach SMP System. Das
waere natuerlich recht interessant, um zu sehen, wie die Modelle bei vielen
CPUs skalieren. Ich kann ausserdem (ausser Konkurrenz natuerlich) noch Daten
fuer Windows und OS/2 liefern, weil das bei meinem Programm kein zusaetzlicher
Aufwand ist.
Waere das nicht ein vernuenftiger Vorschlag zur Versachlichung der Diskussion,
der nicht mal mit uebermaessig viel Arbeit verbunden waere?
> damit dieses Cache-Pingpong auftritt (so auf die Schnelle hab
> ich Schwierigkeiten mir was praxisrelevantes vorzustellen,
> bei dem mehrere CPUs ständig die Daten aus der gleichen Cacheline
> verwuerfeln).
Ein Standardbeispiel ist zB ein oft gebrauchter Mutex, dervon
verschiedenen Threads auf unterschiedlichen CPUs benutzt wird. Jedesmal
wenn ein Thread auf den Mutex zugreift muss er die Cacheline mit dem Mutex
von der CPU holen die ihn zuletzt benutzt hat, um sie zu beschreiben.
Fuer ein gutes Papier darueber siehe zB
http://lwn.net/2001/features/OLS/pdf/pdf/read-copy.pdf
Ein anderes haeufiges Problem ist `false sharing': du hast zwei globale
Variablen die zufaellig auf die gleiche Cacheline fallen. Obwohl sie nicht
direkt was miteinander zu tuen haben koennen die CPUs die Daten nur in
Cachelinegroesse austauschen. Wenn zwei Threads die beiden Variablen
gleichzeitig beschreiben kommt es zu staendigem Ping-Pong.
-Andi
Mich, weil ich das am Ende verstehen, auf Sicherheitslücken untersuchen
und eventuell sogar warten soll.
> > Threads _sind_ eine sehr schlechte Idee.
> Schön. Falls sich also jemand gerne welche aufladen möchte, dann laß
> ihn doch einfach.
Wenn derjenige seinen Code dann niemandem sonst gibt, ist das kein
Thema. Aber guck dir doch mal die Werbeeinblendungne bei Ullrich ein,
die verweisen auf eine Kommerz-Site, d.h. er wird bei ihn
beauftragenden Firmen Sondermüll in nicht unerheblichem Umfang
hinterlassen, mit dessen Wartung irgendwann mal jemand anderes
beauftragt werden wird, wenn er nicht garantiert länger lebt als sein
Code.
> Tendenziell wird er/sie dann nicht funktionierenden
> Schrott produzieren, der im Laufe der Zeit von alleine austirbt.
Ja, genau wie nicht funktionierender Schrott wie Windows, Outlook,
Exchange und Konsorten aussterben... :-(
Felix
Der Effekt für das Programm ist der gleiche wie überhaupt keine
Cache-Kohärenz. Und natürlich gleichen sich Caches irgendwann schon aus
statistischen Gründen aus, wenn nur genug RAM angefaßt wird ;)
Es gibt ja Experten aus der Windows-Ecke, die meinen, sie müßten
Semaphoren selber implementieren, weil die vom System so langsam sind.
Die deklarieren dann "volatile int mutex" oder so und basteln lustig
darauf herum. Da zeigen Lesezugriffe Daten von anno dazumal, da gehen
Schreibzugriffe ganz verloren, und Locks locken halt nicht. Lach nicht,
habe ich mit meinen eigenen staunenden Augen gesehen.
SMP-Architekturen skalieren einfach nicht, wenn es volle Hardware
Cachekohärenz gibt. Sun skaliert überhaupt nur deshalb bei SMP, weil
sie nicht Snoopen und sich nicht ständig gegenseitig automatisiert die
Caches invalidieren.
> Es gibt zwar Speicherinterfaces, die nicht Cachecoherent sind (manche
> der Clusterlinks), aber die werden normal nur mit speziellen
> Funktionsaufrufen und nicht normalen Threads eingestzt.
Ich hab das auf einer Dualprozessor Sparcstation 10 erlebt.
Felix
Bestreitest du das etwa?
Fakt ist, daß du Threads nicht nur benutzt, sondern aktiv anpreist,
während niemand hier C anpreist. Daß dir diese Billig-Rhetorik nicht
peinlich ist!
> PIPE_BUF ist ueblicherweise eine MMU Page. Auf ix86 Architekturen sind das
> 4KB, auf anderen Architekturen mehr oder (seltener) auch weniger. Das
> bedeutet:
> 1. Du musst immer eine volle Seite an Daten durch die Pipe schieben, auch
> dann, wenn die "Nutzlast" nur 20 Bytes betraegt.
Wot?! Das ist ja grotesk! Merkst du eigentlich überhaupt noch was?
> 2. Der ganze Mechanismus funktioniert nicht mehr, wenn Du mehr als PIPE_BUF
> Daten hast. Er ist also nicht universell verwendbar, und Du musst schnell
> auf andere, wesentlich unbequemere Mechanismen (SHM o.ae.) ausweichen.
Was ist an zwei Pipes und poll() unbequem?
> Das funktioniert aber nur in den oben beschriebenen, recht engen Grenzen und
> ist deshalb in vielen Faellen ueberhaupt nicht verwendbar. Ausserdem wird je
> nach Nachrichtengroesse eine irrsinnige Menge Muell durch die Gegend kopiert.
Ah ja, Beweis durch wiederholte Behauptung.
War ja zu erwarten.
Nein. Bei getrennten Prozessen musst du nur deinen eigenen Code
reentrant machen, und auch das nur in Spezialfällen (z.B. Rekursion).
Bei fremden Funktionen musst du nur aufpassen, wenn sie mit
irgendwelchen static-Variablen herumfuhrwerken.
Bei Threads hingegen reicht ein einfaches malloc(). Bei Threads hast du
die paar Reentranzprobleme, die du auch bei einzelnen Prozessen hast,
_plus_ noch einen ganzen Sack voller Probleme, die erst durch Threads
entstehen.
> Wenn einer Deiner Prozesse abraucht steht das System genauso. Und ich wuerde
> in beiden Faellen Vorkehrungen treffen, den oder die Prozesse wieder zu
> starten - Threads oder nicht.
Der Unterschied ist nur: wenn ein Thread abraucht, weil er wild im
Speicher rumgeschrieben hat, dann verhalten sich _alle_ Threads nicht
mehr deterministisch, du musst also deinen kompletten Server abschiessen
und neu starten.
Bei getrennten Prozessen dagegen ist man nur ein fork() weit davon
entfernt, einen einzelnen abgerauchten Worker wieder zu ersetzen
(kritischer wird es, wenn der einzelne Receiver abraucht, bei geeignetem
Design kann man aber auch hier Vorkehrungen schaffen).
> * Threads erlauben eine einfachere Abstimmung zwischen den verschiedenen
> Teilen des Programms.
Sie erlauben eine unsaubere Synchronisation zwischen den Programmteilen.
Auf den ersten Blick mag das manchmal einfacher erscheinen, aber wehe,
es schleicht sich dabei ein Fehler ein.
Bei Prozessen bist du dagegen auf bestimmte, exakt umrissene
IPC-Möglichkeiten festgelegt. Das hält vielmehr dazu an, die
Kommunikation ordentlich zu gestalten, was am Ende der Stabilität und
Wartbarkeit zugute kommt.
> * Sie sind beim heutigen Stand der Technik portabler. Ich habe es nicht
> probiert, weil ich nicht dafuer bezahlt wurde, aber ich gehe davon aus,
> dass sich mein Programm in etwa einem Tag auf Windows portieren laesst,
> falls es mal notwendig sein sollte.
Ich habe noch keine Anwendungen auf Windows portiert, und habe auch
nicht vor, diesen Umstand zu ändern. Aber wegen Cygwin bezweifle ich,
daß die Prozess-Variante wesentlich länger zur Portierung braucht (eher
weniger).
> Alle Spezialitaeten wie Threads,
> Synchronisationsmechanismen sind gekappselt, und existieren auch in
> einer Version fuer Windows, der Code selber enthaelt keine
> betriebssystemspezifischen Routinen.
Auch fork() & Co. sind ordentlich "gekapselt": auf Unix sind sie native
implementiert, und für Windows existiert eine Compatibility-Library.
>>> Das ist dann nicht richtig, wenn andere Prozesse auf dem Rechner laufen, die
> [...]
>> Wenn dein System zusammenbricht, bloss weil einmal alle darauf laufenden
>> Dienste für längere Zeit Vollast fahren, dann machst du was flasch.
> Warum gehst Du nicht einfach auf meinen Einwand ein? Ich habe Dir erklaert,
> warum meine Massnahme Sinn macht. Ein Server mit dieser Aenderung laeuft auch
> bei wechselnden Lasten auf dem Server performanter als einer ohne.
Diese Diskussion ist müssig. Einigen wir uns auf ein "It depends"? Es
werden sich sicher Situationen finden lassen, in denen jeweils die eine
oder die andere Vorgehensweise sinnvoller ist.
Jedoch sollte man IMO zunächst immer die am wenigsten komplexe Variante
probieren (das wäre in diesem Fall die ohne eigene Send-Queue), und erst
wenn diese sich als Bottleneck herausstellt, kann man schrittweise
Komplexität hinzufügen. "Premature optimization is the root of all
evil".
> Fuer den Fall, dass Du vielleicht doch mal in die Verlegenheit kommen solltest
> habe ich nachgeschaut, wie gross der zusaetzliche Aufwand ist: Bei mir
> betraegt er ganze 150 Zeilen Code (beim Schreiben einer Nachricht).
Mir ist auch klar, daß die Verwaltung einer Send-Queue keinen
übermässigen Aufwand bedeutet. Dennoch gilt für mich KISS, auch wenn der
Aufwand noch so klein ist, er wird erst gemacht, wenn er sich als
absolut notwendig erwiesen hat.
> Das Empfangsteil und Sendeteil, d.h. der Teil des Programms, der mit den
> Clients redet, die Clientliste verwaltet etc. (und nur um diesen ging es hier,
> von der Verarbeitung hat niemand geredet) laesst sich sehr gut als separate
> Bibliothek schreiben, und das habe ich bei mir auch so gemacht. Ob Du UDP oder
> TCP machst bedingt nur sehr wenige Aenderungen beim Erzeugen und teilweise
> beim Behandeln der Sockets.
So wie ich den OP verstanden habe ist sein Protokoll im wesentlichen
stateless, von irgendeiner Form von "Sessions" war nicht die Rede.
Natürlich kann man die Routinen, die die eigentliche Request-Bearbeitung
machen, in einer Library kapseln. Hier ging es aber in erster Linie
darum, wie man die eintreffenden Requests am besten entgegennimmt und
die Verarbeitung koordiniert, was da im einzelnen verarbeitet wird ist
irrelevant, und dabei macht es sehr wohl einen grossen Unterschied, ob
man ein TCP- oder ein UDP-basiertes Protokoll hat. Mit anderen Worten...
> Diese lassen sich in meinem Fall (weil C++) leicht
> durch virtuelle Methoden dem jeweiligen Netzwerksprotokoll anpassen.
...geht es hier genau um diese virtuellen Methoden, nicht um den
Verarbeitungs-Part.
Andreas
--
Andreas Ferber - dev/consulting GmbH - Bielefeld, FRG
---------------------------------------------------------
+49 521 1365800 - a...@devcon.net - www.devcon.net
Bei einer (gemeinsamen) Message-Queue stellt sich das Problem ja nicht,
da ja jeder Worker immer das nächste Paket aus der Queue nehmen kann,
wenn er mit seinem letzten fertig ist.
Bei Pipes dagegen musst du entweder im Pipebuffer ein Backlog aufbauen
und dabei inkaufnehmen, daß evtl. die Latenzen stark schwanken können,
oder du musst die Worker an den Receiver rückmelden lassen, wenn sie das
nächste Paket bearbeiten können, und letzterer teilt ihnen erst dann
wieder ein Paket zu. Inwiefern Timestamps dabei helfen sollten ist mir
allerdings nicht ganz klar.
>> Du musst sowieso ein Programm schreiben, das Testpakete generiert, ob
>> diese dann nach stdout geschrieben werden oder im Shared Memory abgelegt
>> werden ist IMHO nebensaechlich ;-)
> Aehm, ob ich das mit netcat und einem Shell-Skript oder als
> C-Programm mache ist fuer mich ein signifikanter Unterschied ;-)
Hmm, OK. Setzt voraus, daß das Protokoll mehr oder weniger Klartext ist.
>> Ack, an Message Queues hatte ich nicht gedacht. Allerdings kann ein
>> UDP-Paket groesser als MSGMAX sein.
> Man hat ja MSG-Nummern. Da kann man Fragmente mit basteln
> (pervers, aber moeglich).
BTDT, redesigned it ;-)
> Oder man akzeptiert keine UDP-Pakete >
> MSGMAX [1] und sieht diese Grenze bereits im Protokoll vor.
Wird problematisch, s.u. die Anmerkung zu deiner Fussnote.
>> Um einen Slot wieder freizugeben, muss der Worker dann ein mit
>> dem Slot assoziiertes Flag im Shared Memory auf Null setzen (das geht
>> atomar), belegt werden Slots nur vom Receiver.
> Nee, das kann man auch gleich ueber Messages machen. Atomare
> Operationen im Userspace gibt es nicht auf jeder Architektur.
Atomare Operationen sind aber auch garnicht nötig. Zur Freigabe des
Slots ist genau eine Speicheradresse zu überschreiben. Gibt es
irgendeine Architektur, wo der Speicherwert an der Adresse bei einer
solchen Operation mehrfach zwischen altem und neuem Wert hin- und
herwechselt? Wenn die Änderung erst verzögert im System propagiert
(Write Back Caches z.B.), ist das kein Problem, das einzige was
passieren kann ist, daß der Receiver erst verspätet von der Freigabe des
Slots erfährt, und damit länger mit der Wiederbenutzung wartet, als
eigentlich nötig wäre.
Alternativ kann man aber den gleichen IPC-Mechanismus, der benutzt wird,
um die Slots an die Worker zu verteilen, in umgekehrter Richtung auch
nutzen, um die Slots dem Receiver wieder zurückzugeben.
Ein anderes Problem dieses Ansatzes wurde aber noch nicht genannt: Wenn
ein Worker abstirbt, bevor er seinen aktuellen Slot an den Receiver
zurückgeben konnte, dann bleibt dieser Slot bis in alle Ewigkeit belegt.
Auch dieses Problem liesse sich lösen, das wird dann aber aufwendig und
(IMO) unschön.
> Die Worker sollten das ausgleichen. Bei stark schwankenden
> Bearbeitungszeiten ist ein Redesign faellig.
Jein. Es gibt auch Situationen, wo die Bearbeitungszeit nunmal stark von
den zu bearbeitenden Daten abhängt, da kann man auch mit einem guten
Design nicht viel dran ändern, d.h. die Schwankungen müssen bei der
Verteilung der Arbeit berücksichtigt werden (oder man muss eben
schwankende Latenzen inkaufnehmen).
>>> Die Semaphoren des Systems zu benutzen ist auf jeden Fall extrem
>>> langsam. So langsam, dass ein zusaetzlices rumkopieren da meist
>>> nicht auffaellt. Meist bieten die Thread-Pakete da irgendwas
>>> schnelleres, wenn es innerhalb des selben Adreszraumes liegt.
>> Da wird es dann aber schnell architekturabhaengig.
> Nein, lies den Abschnitt von mir nochmal. Das Thread-Paket
> abstrahiert das i.A. ausreichend, wenn der Programmierer weiter
> als bis zu seiner Nasenspitze gedacht hat.
OK, sorum wird ein Schuh daraus. Ich hatte dich so verstanden, daß du
die Synchronisationsmechanismen der Thread-Pakete "abschreiben"
wolltest, da es hier ja (auch) um mögliche Implementierungen unter
Verzicht auf Threads ging.
> Der worst-case ist
> dann die Benutzung der Semaphoren des OS, welches dem von dir
> vorgeschlagenem Mechanismus entspricht ;-)
Ack.
> [1] Ich finde momentan das garantierte Minimum fuer MSGMAX nicht.
> Kann das mal jmd. ergaenzen? Danke!
SUSv2 spricht nur von einem "system-imposed limit", ein Minimalwert
scheint aber nicht vorgegeben zu werden.
> PIPE_BUF ist ueblicherweise eine MMU Page. Auf ix86 Architekturen sind das
> 4KB, auf anderen Architekturen mehr oder (seltener) auch weniger. Das
> bedeutet:
> 1. Du musst immer eine volle Seite an Daten durch die Pipe schieben, auch
> dann, wenn die "Nutzlast" nur 20 Bytes betraegt.
Nein, das stimmt nicht, denn die Zugriffe sind dann
atomar, wenn die Größe der Datenmenge <= PIPE_BUF ist,
und nicht nur wenn sie == PIPE_BUF ist.
[...]
> Nimm Dein eigenes Programm als Beispiel: Tatsaechlich uebertragen willst
> Du 12 Zeichen ("Hello Child\n"). Auf meinem Rechner kopierst Du aber 4KB
> (best case) bzw. 8KB (worst case). Diese unsinnige Kopiererei verdraengt
> Daten aus dem Cache, die spaeter durch teure Speicherzugriffe wieder
> nachgeladen werden muessen. Insofern halte ich die von Dir propagierten
> Cache Probleme bei Threads fuer bestenfalls theoretisch.
Das trifft deshalb nicht zu.
> 2. Der ganze Mechanismus funktioniert nicht mehr, wenn Du mehr als PIPE_BUF
> Daten hast. Er ist also nicht universell verwendbar, und Du musst schnell
> auf andere, wesentlich unbequemere Mechanismen (SHM o.ae.) ausweichen.
Tja, dann ist es nicht mehr atomar, das stimmt.
BTW: Threads bieten aber auch keine atomaren Zugriffe.
Das bedeutet, daß die Pipes mit Datenmenge <= PIPE_BUF sogar
noch strengeren Maßstäben genügen.
allerdings oberhalb von PIPE_BUF hat man da schon so Problemchen.
Ist aber noch die Frage, ob man sein Programmdesign nicht
so gestalten kann, daß man immer mit Datenmengen <= PIPE_BUF zurecht
kommt.
> 3. Mir ist nicht bekannt, ob es eine Mindestgroesse fuer PIPE_BUF gibt, im
> Stevens ist jedoch angefuehrt, dass PIPE_BUF fuer BSD 4.3+ den Wert 512
> hat. Eventuell liegt die garantierte Mindestgroesse darunter, spaetestens
> aber, wenn Du mehr als 512 Bytes hast, die Du verschieben willst, dann
> funktioniert Dein Mechanismus nicht mehr, bzw. ist zumindest nicht mehr
> zwischen den verschiedenen Unixen portabel.
Wenn man mit symbolischen Variablen arbeitet, dann sollte
das keine prinzipiellen Probleme bereiten. Aber die Sache
mit der atomar übertragbaren Datenmenge ist dann schon
so eine Sache.
Aber wenn man diesen mechanismus bloß als Synchronisationsmechanismus
einsetzt und nicht als Datenschleuder, dann sollte es ausreichend
sein.
[...]
> Unter der Annahme, dass der Producer 1000 Nachrichten pro Sekunde generiert
> (fuer ein 32-fach SMP System ist das eher wenig) kopiert Deine Loesung im
> Gegensatz zu meiner auf gaengiger Hardware etwa 4-8 Megabyte(!) pro Sekunde
> durch die Gegend. Je nach Nachrichtengrosse ist der Hauptteil davon Muell, in
> Deinem Beispielprogramm zum Beispiel mehr als 99,7% der Daten. Dieser Muell
> bewirkt, dass
Deine Annahme mit dem Overhead ist nicht richtig; Erklärung siehe oben.
[...]
> Aber wie waer's denn mit folgendem Vorschlag: Wir schreiben beide ein
> Programm. Deines verwendet Prozesse und Pipes, meines verwendet Threads und
> Mailboxen. Zielplattform ist Unix (mit POSIX Threads in meinem Fall).
Ja, macht das mal. :-)
[...]
> Ich denke, dass ich das in einer halben Stunde hinbekomme. Mit Prozessen geht
> das ja angeblich noch einfacher als mit Threads, Du solltest also eher weniger
> Zeit benoetigen.
[...]
Ja, Aufwand an Code und Zeit sollte auch gemessen werden.
Aber bitte ehrlich sein....
...wer auf seine schon vorhandenen Libs zurück greift,
ist dann ja eigentlich schon ein schummler.
Also: Bitte alles neu schreiben...
Ciao,
Oliver
--
You are looking for freelancers/programmers/developers? You have remote-jobs?
=> http://www.belug.org/~ob/CV.html
Voellig korrekt, das ist mir gestern irgendwann auch noch aufgefallen.
Insofern sind alle meine Anmerkungen, die sich darauf beziehen natuerlich
Quatsch. Tut mir leid, mein Fehler. Auf den Rest meiner Einwaende hat das
allerdings keine Auswirkungen,
Anmerkung am Rande: Ingos Beispielprogramm schiebt tatsaechlich den ganzen
Puffer mit der Groesse PIPE_BUF durch die Pipe. Das ist aber ein Fehler bzw.
eine Nachlaessigkeit in seinem Programm und muss nicht so sein.
> BTW: Threads bieten aber auch keine atomaren Zugriffe.
Wenn Du Datenbloecke via Message Passing verschiebst, dann ist das kein
Problem. Fuer mein Programm ist die Groesse der Daten letzten Endes wurscht,
auch wenn es irgendwo aufhoert, wirklich Sinn zu machen.
> Ist aber noch die Frage, ob man sein Programmdesign nicht
> so gestalten kann, daß man immer mit Datenmengen <= PIPE_BUF zurecht
> kommt.
Das ist eine sehr philosophische Frage: Willst Du Dich von aeusseren
Gegebenheiten, d.h. der Programmiersprache, dem Betriebssystem, den
verwendeten Tools usw. auf bestimmte Designs und Algorithmen einschraenken
lassen, oder suchst Du Dir Deine Sprache, Deine Tools usw. lieber so aus, dass
sie zu Deinen Algorithmen passen?
Ich persoenlich tendiere zu letzterem. Ich verwende je nach Projekt (und auch
dort gemischt) verschiedene Programmiersprachen, suche mir meine Bibliotheken
am liebsten so aus, dass sie zu meiner Aufgabenstellung passen und passe nicht
die Aufgabenstellung an, usw. Wenn Dich die Umgebung nicht auf bestimmte
Designs einschraenkt, dann hast Du in meinen Augen mehr Chancen, ein konkretes
Design vernuenftig hinzukriegen.
> Ja, Aufwand an Code und Zeit sollte auch gemessen werden.
> Aber bitte ehrlich sein....
Aufwand an Code laesst sich leichter messen, Aufwand an Zeit ist schlecht
vergleichbar. Es gibt eine Menge Studien in der Richtung: Die Leistungen von
unterschiedlichen Programmierern unterscheiden sich bis hin zum Faktor 10
(siehe z.B. "Wien wartet auf Dich! Der Faktor Mensch im DV-Management" von Tom
DeMarco und Timothy Lister, erschienen im Hanser Verlag). Wenn Du beim Faktor
Mensch schon so grosse Unterschiede hast, dann kannst Du Zeitaufwand fuer
verschiedene Ansaetze, wenn sie von verschiedenen Leuten implementiert werden
eben nicht mehr vergleichen.
> ...wer auf seine schon vorhandenen Libs zurück greift,
> ist dann ja eigentlich schon ein schummler.
> Also: Bitte alles neu schreiben...
Das Argument ist meiner Meinung nach falsch. Ich schreibe ja schliesslich
deshalb Bibliotheken, weil ich dann den Aufwand nur einmal habe. D.h. in einem
realen Projekt wuerde ich die Sachen ja auch nicht neu schreiben. Den Code,
der in der glibc oder in der curses library steckt wuerdest Du ja auch nicht
als Teil eines Projekts mit veranschlagen, sondern Du nimmst ihn einfach als
gegeben hin.
Ich wuerde deshalb vorschlagen, den Versuch einer Zeitmessung unter den Tisch
fallen zu lassen, weil das nicht realistisch umzusetzen ist. Den Code fuer die
Programme und die verwendeten Bibliotheken kann sich selber jeder anschauen,
damit kann jeder auch selber vergleichen, was wieviel Aufwand verursacht hat,
wieviel Vorarbeit (in Form von Bibliotheken) notwendig war, und wieviel
Aufwand die Loesung des konkrete Problems verursacht hat.
> Der Unterschied ist nur: wenn ein Thread abraucht, weil er wild im
> Speicher rumgeschrieben hat, dann verhalten sich _alle_ Threads nicht
> mehr deterministisch, du musst also deinen kompletten Server abschiessen
> und neu starten.
>
> Bei getrennten Prozessen dagegen ist man nur ein fork() weit davon
> entfernt, einen einzelnen abgerauchten Worker wieder zu ersetzen
> (kritischer wird es, wenn der einzelne Receiver abraucht, bei geeignetem
> Design kann man aber auch hier Vorkehrungen schaffen).
Ich glaube nicht, daß es irgendeinen Sinn ergibt, auf fehlerhaften
Umgang mit Zeigern Rücksicht zu nehmen. Fast jeder Mißbrauch von
Zeigern ist gleichzeitig ein Sicherheitsproblem. Falls die Software
trotz derartiger Probleme weiterläuft, hält das nur die Leute davon
ab, Fixes einzuspielen.
> Ich habe noch keine Anwendungen auf Windows portiert, und habe auch
> nicht vor, diesen Umstand zu ändern. Aber wegen Cygwin bezweifle ich,
> daß die Prozess-Variante wesentlich länger zur Portierung braucht (eher
> weniger).
fork() war einst unter Cygwin *extrem* teuer. Keine Ahnung, ob sich
das geändert hat.
> Threads skalieren schlechter als Prozesse auf SMP-Systemen.
> Bei Prozessen kriegt jeder Prozess schreibbare Seiten für sich alleine
> (außer natürlich du hebelst das durch shared memory aus, was nicht zu
> empfehlen ist). Daraus folgt, daß die anderen Prozessoren nicht ständig
> in ihrem Cache heruminvalidieren müssen.
Und wenn man die Daten im selben Adreßraum separiert, hat das nicht
denselben Effekt?
> Auf manchen Plattformen (sparc comes to mind) gibt es keine
> Cache-Kohärenz in Hardware, so daß shared memory nur geht, indem man
> entweder Semaphoren des Systems benutzt oder in dem Bereich den Cache
> ganz abschaltet. Performance, nein danke.
Werden auf solchen Architekturen die Caches beim Kernel-Eintritt
synchronsiert? Oder nur bei bestimmten Syscalls (z.B. flock())?
> Thus spake Florian Weimer (f...@deneb.enyo.de):
> > > shm ist aber unbequem genug das es sich jeder 2-3x ueberlegt bevor er es
>> > benutzt. Das ist eine gute Sache.
>> Und was benutzt man dann zum Datenaustausch? Pipes?
>
> Ja, natürlich.
Interessant. War nicht ich derjenige, der 50% der Geschwindigkeit für
mehr Zuverlässigkeit opfern wollte? ;-)
> Es gibt ja Experten aus der Windows-Ecke, die meinen, sie müßten
> Semaphoren selber implementieren, weil die vom System so langsam sind.
Die von Dir so geschätzte ;-) Berkeley DB macht das im übrigen auch.
> Selbst wenn die Threads voellig andere Variablen benutzen, so
> spielen sie Cache-Pingpong, wenn diese Variablen zufaellig in der
> selben Cacheline liegen
Das ist aber kein wesentlich neues Problem, oder? Cache-Trashing durch
zwei oder mehr Prozesse durch Verwendung ungünstiger Adressen kann
doch auch auf Ein-Prozessor-Maschinen auftreten.
> (z.B. 2 sehr kleine Objekte wurden mit "new" kurz hintereinander
> alloziert, dank Polymorphismus kann das auftreten).
Das wiederum sollte nichts ausmachen, wenn man einen vernünftigen
Allokator verwendet. ;-)
Wo ist das Problem mit malloc()? Jede multithreaded Umgebung bietet ein
malloc() an, das threadsafe ist.
> Bei Threads hast du
> die paar Reentranzprobleme, die du auch bei einzelnen Prozessen hast,
> _plus_ noch einen ganzen Sack voller Probleme, die erst durch Threads
> entstehen.
Du hast bei Threads mehr potentielle *Moeglichkeiten*, dass was schiefgeht und
bist deshalb mehr darauf angewiesen, dass Dein Code korrekt ist. Damit war ich
schon immer einig. Die Frage ist, ob sich diese Moeglichkeiten in echte
Probleme uebersetzen, oder nicht. Damit das nicht passiert, zumindest nicht in
einem kritischen Mass habe ich bereits mehrfach Methoden propagiert, die
helfen, das zu vermeiden.
> Der Unterschied ist nur: wenn ein Thread abraucht, weil er wild im
> Speicher rumgeschrieben hat, dann verhalten sich _alle_ Threads nicht
> mehr deterministisch, du musst also deinen kompletten Server abschiessen
> und neu starten.
>
> Bei getrennten Prozessen dagegen ist man nur ein fork() weit davon
> entfernt, einen einzelnen abgerauchten Worker wieder zu ersetzen
> (kritischer wird es, wenn der einzelne Receiver abraucht, bei geeignetem
> Design kann man aber auch hier Vorkehrungen schaffen).
Es ist normalerweise einfacher, alles zu terminieren und neu zu starten, als
innerhalb vom Programm herauszufinden, welchen Fehler man in das Programm
eingebaut hat, und darauf zu reagieren. Mein Verstaendnis, was solche Fehler
angeht ist: Wenn ein Programm fehlerhaft ist, dann kann ich ihm an der Stelle
auch nicht mehr trauen, wenn es um die Entscheidung geht, was aus welchen
Gruenden schiefgegangen ist, deshalb muss ich die Fehlerbehebung/ -umgehung um
mindestens eine Stufe weiter nach oben verlagern. (Das gilt natuerlich nicht
bei Fehlern, die ausserhalb des Programms liegen, aber um die geht es hier
nicht).
> Bei Prozessen bist du dagegen auf bestimmte, exakt umrissene
> IPC-Möglichkeiten festgelegt. Das hält vielmehr dazu an, die
> Kommunikation ordentlich zu gestalten, was am Ende der Stabilität und
> Wartbarkeit zugute kommt.
Die Moeglichkeiten, die Du mit Threads hast sind eine Obermenge derjenigen,
die mit Prozessen verfuegbar sind (ich kann mit Threads wenn ich will auch
Pipes verwenden, nur als Beispiel). Die Frage ist, ob Du beim Entwerfen einer
Loesung fuer ein bestimmtes Problem von vorneherein bestimmte Loesungs-
moeglichkeiten ausschliessen willst, weil Deine Umgebung sie nicht moeglich
macht. Traditionell haben C Programmierer unter Unix solche Einschraenkungen
eher abgelehnt, die Philosopie war "Gib mir alle Moeglichkeiten, wenn ich mich
dann in den Fuss schiesse, ist das mein Problem". Das ist fuer mich der Grund,
warum ich C fuer eine gute Sprache fuer viele Probleme halte, warum ich Unix
mag, und auch warum ich Threads mag. Sie geben mir viel Freiheit, die aber
selbstverstaendlich auch missbraucht werden kann. Die Verantwortung, diese
Freiheiten nicht zu missbrauchen liegt aber bei mir.
> Ich habe noch keine Anwendungen auf Windows portiert, und habe auch
> nicht vor, diesen Umstand zu ändern. Aber wegen Cygwin bezweifle ich,
> daß die Prozess-Variante wesentlich länger zur Portierung braucht (eher
> weniger).
Das letzte Mal als ich mir Cygwin angeschaut habe war die nowendige DLL 6MB(!)
gross, hat die ueblichen Versionsprobleme gehabt, und es wurde extra darauf
hingewiesen, dass fork() zwar in den meisten Faellen funktionieren wuerde,
dass es aber extrem ineffektiv sei, und man deshalb keine Anforderungen an die
Performance stellen sollte. Vielleicht hat sich das ja geaendert, fuer mich
klang es damals auf jeden Fall nicht wie etwas, was ich meinen Kunden
unbedingt empfehlen sollte.
> Mir ist auch klar, daß die Verwaltung einer Send-Queue keinen
> übermässigen Aufwand bedeutet. Dennoch gilt für mich KISS, auch wenn der
> Aufwand noch so klein ist, er wird erst gemacht, wenn er sich als
> absolut notwendig erwiesen hat.
KISS ist ok, aber auch hier gibt es nicht nur schwarz und weiss.
Implementierst Du den "--help" Kommandozeilenparameter auch erst dann, wenn
verzweifelte Anfragen von Benutzern kommen, die sich wundern, warum Dein
Programm nicht dazu zu ueberreden ist, auch nur den kleinsten Hinweis auf die
Benutzung zu geben? Das waere KISS in Reinform. In der Realitaet ist es wohl
immer eine Abwaegung: Welche Vorteile habe ich auf der einen Seite und wie
gross ist auf der anderen Seite die Chance, dass ich Fehler in den Code mache.
Insofern gilt Dein "It depends" auch hier:-)
> So wie ich den OP verstanden habe ist sein Protokoll im wesentlichen
> stateless, von irgendeiner Form von "Sessions" war nicht die Rede.
Korrekt. Mein Ansatz ist aber normalerweise: Kann ich die Loesung fuer ein
Problem so allgemein halten, dass ich sie das naechste Mal bei einer
aehnlichen Aufgabenstellung wiederverwenden kann? Wenn ich mir darueber keine
Gedanken mache, dann kommt ein zu Fuss programmierter Quicksort raus, mache
ich mir Gedanken darueber, dann entsteht eher sowas wie qsort() aus der
Standard Libc.
> Hier ging es aber in erster Linie
> darum, wie man die eintreffenden Requests am besten entgegennimmt und
> die Verarbeitung koordiniert, was da im einzelnen verarbeitet wird ist
> irrelevant, und dabei macht es sehr wohl einen grossen Unterschied, ob
> man ein TCP- oder ein UDP-basiertes Protokoll hat.
Was verarbeitet wird ist irrelevant, aber es kommt dabei sehr wohl auf das
Protokoll an? Das klingt fuer mich wie ein Widerspruch. Es ist wurscht, was
verarbeitet wird und es ist auch egal, woher es kommt und wie es uebertragen
wird. Das ist doch der ganze Trick bei der Abstrahierung: Dass ich eben genau
diese Details aussen vor lasse. Natuerlich hat auch diese Abstraktion Grenzen,
aber UDP und TCP lassen sich relativ leicht unter einen Hut bringen, warum
sollte ich mich dann auf eines der beiden festlegen? Wobei die Anmerkungen
ueber KISS natuerlich auch hierfuer gelten:-)
> > Auf manchen Plattformen (sparc comes to mind) gibt es keine
> > Cache-Kohärenz in Hardware, so daß shared memory nur geht, indem man
> > entweder Semaphoren des Systems benutzt oder in dem Bereich den Cache
> > ganz abschaltet. Performance, nein danke.
>
> Werden auf solchen Architekturen die Caches beim Kernel-Eintritt
> synchronsiert? Oder nur bei bestimmten Syscalls (z.B. flock())?
Felix hat halluziniert. Sparc ist normal cache coherent.
[nur die Schreibreihenfolge ist nicht garantiert ohne Memory barriers,
das ist sie aber bei den meisten neueren Architekturen nicht mehr]
-Andi
> > Werden auf solchen Architekturen die Caches beim Kernel-Eintritt
> > synchronsiert? Oder nur bei bestimmten Syscalls (z.B. flock())?
>
> Felix hat halluziniert. Sparc ist normal cache coherent.
> [nur die Schreibreihenfolge ist nicht garantiert ohne Memory barriers,
> das ist sie aber bei den meisten neueren Architekturen nicht mehr]
Okay, und eine memory barrier wird ein Kernel-Eintritt wohl sein.
>> Felix hat halluziniert. Sparc ist normal cache coherent.
>> [nur die Schreibreihenfolge ist nicht garantiert ohne Memory barriers,
>> das ist sie aber bei den meisten neueren Architekturen nicht mehr]
>Okay, und eine memory barrier wird ein Kernel-Eintritt wohl sein.
Wird sie? Wie definiert die entsprechende Architektur den dort
verwendeten Trap?
Gruss
Patrick "rein hypothetisch" S.
Windows ist halt scheiße. Das kann auch cygwin nicht ändern.
Die fork()-Emulation setzt u.a. einen Breakpoint, um die Unix-Semantik
erfüllen zu können. Klar performt das nicht, selbst wenn unter Windows
Prozeßerzeugung an sich performen würde (was es nicht tut).
Felix
Hey, selbst _X_ benutzt Sockets statt shared memory und performt
akzeptabel schnell. Ja, es gibt _auch_ shared memory. Aber das
benutzen nur wenige Anwendungen (außer die Bastard-Xlib von HP
natürlich)
Felix
Wo liegt denn dann noch der "Vorteil" von Threads?
Felix
> Voellig korrekt, das ist mir gestern irgendwann auch noch aufgefallen.
> Insofern sind alle meine Anmerkungen, die sich darauf beziehen natuerlich
> Quatsch. Tut mir leid, mein Fehler. Auf den Rest meiner Einwaende hat das
> allerdings keine Auswirkungen,
[...]
>> Ist aber noch die Frage, ob man sein Programmdesign nicht
>> so gestalten kann, daß man immer mit Datenmengen <= PIPE_BUF zurecht
>> kommt.
> Das ist eine sehr philosophische Frage: Willst Du Dich von aeusseren
> Gegebenheiten, d.h. der Programmiersprache, dem Betriebssystem, den
> verwendeten Tools usw. auf bestimmte Designs und Algorithmen einschraenken
> lassen, oder suchst Du Dir Deine Sprache, Deine Tools usw. lieber so aus, dass
> sie zu Deinen Algorithmen passen?
Ich gucke erst mal, was ich realisieren will und schau dann,
was dazu notwendig ist.
Da kann man weder mit den Algorithmen noch mit den Sprachen/Libs/OSs
anfangen. Man schaut, was man braucht, dann was verfügbar ist
und muß ständig mal hier und mal da schauen, wo man was sinnvolles
zusammenbauen kann.
Der Algorithmus als Ausgangspunkt nützt auch nichts, wenn man ihn
nicht implementieren kann; ebenso, wie eine tolle Sprache auch noch
kein Programm macht.
[...]
>> Ja, Aufwand an Code und Zeit sollte auch gemessen werden.
>> Aber bitte ehrlich sein....
> Aufwand an Code laesst sich leichter messen, Aufwand an Zeit ist schlecht
> vergleichbar. Es gibt eine Menge Studien in der Richtung: Die Leistungen von
> unterschiedlichen Programmierern unterscheiden sich bis hin zum Faktor 10
> (siehe z.B. "Wien wartet auf Dich! Der Faktor Mensch im DV-Management" von Tom
> DeMarco und Timothy Lister, erschienen im Hanser Verlag). Wenn Du beim Faktor
> Mensch schon so grosse Unterschiede hast, dann kannst Du Zeitaufwand fuer
> verschiedene Ansaetze, wenn sie von verschiedenen Leuten implementiert werden
> eben nicht mehr vergleichen.
Hmhhh....
>> ...wer auf seine schon vorhandenen Libs zurück greift,
>> ist dann ja eigentlich schon ein schummler.
>> Also: Bitte alles neu schreiben...
> Das Argument ist meiner Meinung nach falsch. Ich schreibe ja schliesslich
> deshalb Bibliotheken, weil ich dann den Aufwand nur einmal habe. D.h. in einem
> realen Projekt wuerde ich die Sachen ja auch nicht neu schreiben.
Das ist richtig. Aber heir geht's ja nicht um ein "reales", sondern
ein "theoretisches" Projekt.
Wenn einer von Euch schon für die spezielle Anwendung eine Bibliothek
geschrieben hat, der andere nicht, dann vergleicht man tatsächlich
Äpfel mit Birnen (oder sogar Tomaten?!).
Und gerade deswegen darf man dann nicht auf selbst geschriebene
Bibliotheken zurück greifen, den sie verzerren die Voraussetzungen,
da sie schon im speziellen Fall der Anwendung, die geschrieben
werden soll, einen gezielten Vorsprung bietet.
> Den Code,
> der in der glibc oder in der curses library steckt wuerdest Du ja auch nicht
> als Teil eines Projekts mit veranschlagen, sondern Du nimmst ihn einfach als
> gegeben hin.
Richtig; dieser Code ist aber für alle, die ein solches Projekt
angehen der gleiche; man hat keine *speziellen* Libs,
die man sich selbst persönlich schon mal geschrieben hat.
Wenn alle die gleichen libs bekommen, also C-Lib, dann
Unix-API und dazu POSIX-Threads, aber keine speziellen, die
entweder Prozess- oder Thread-programmierung bevorzugen, dann
erst kann man die Prozess- und Thread-herangehensweise vergleichen.
Hat einer von euch schon mal mehrere mannmomante in die eine oder
andere Variante gesteckt, auf die er bloß noch zurückgreifen
braucht, ist das eine Wettbewerbsverzerrung.
> Ich wuerde deshalb vorschlagen, den Versuch einer Zeitmessung unter den Tisch
> fallen zu lassen, weil das nicht realistisch umzusetzen ist. Den Code fuer die
> Programme und die verwendeten Bibliotheken kann sich selber jeder anschauen,
> damit kann jeder auch selber vergleichen, was wieviel Aufwand verursacht hat,
> wieviel Vorarbeit (in Form von Bibliotheken) notwendig war, und wieviel
> Aufwand die Loesung des konkrete Problems verursacht hat.
Das liesse sich evtl. so auch denken.
> Wo ist das Problem mit malloc()? Jede multithreaded Umgebung bietet ein
> malloc() an, das threadsafe ist.
Da wäre' ich mir mal nicht so sicher.
BTW kann es sein, daß - wenn doch vorhanden - evtl.
ganz und garnicht mehr so performant.
Da sind die Unterschiede in Plattformen/Libs sicherlich
beträchtlich.
[...]
>> Der Unterschied ist nur: wenn ein Thread abraucht, weil er wild im
>> Speicher rumgeschrieben hat, dann verhalten sich _alle_ Threads nicht
>> mehr deterministisch, du musst also deinen kompletten Server abschiessen
>> und neu starten.
>>
>> Bei getrennten Prozessen dagegen ist man nur ein fork() weit davon
>> entfernt, einen einzelnen abgerauchten Worker wieder zu ersetzen
>> (kritischer wird es, wenn der einzelne Receiver abraucht, bei geeignetem
>> Design kann man aber auch hier Vorkehrungen schaffen).
> Es ist normalerweise einfacher, alles zu terminieren und neu zu starten, als
> innerhalb vom Programm herauszufinden, welchen Fehler man in das Programm
> eingebaut hat, und darauf zu reagieren.
Na, das scheint nicht die optimale Debugging-Strategie zu sein,
das programm zu beenden, wenn es Probleme macht.
Zum Fehlersuchen muß das teil schon laufen... es sei denn
man schreibt schön brav megabyteweise Debugging-Logs.
> Mein Verstaendnis, was solche Fehler
> angeht ist: Wenn ein Programm fehlerhaft ist, dann kann ich ihm an der Stelle
> auch nicht mehr trauen, wenn es um die Entscheidung geht, was aus welchen
> Gruenden schiefgegangen ist, deshalb muss ich die Fehlerbehebung/ -umgehung um
> mindestens eine Stufe weiter nach oben verlagern.
Das ist gerade bei Thread-Programmierung typische/notwendige
Vorgehensweise.
Wobei oben aber davon gesprochen wurde, daß etwas abraucht.
Da hast Du dann keine Chance mehr, Dein Programm a) zu beenden
und b) irgendwo anders zu debuggen, denn der Prozess mit Deinen
vielen Threads ist gleich mit abgesemmelt.
Der Vorteil von der Prozess-Herangehensweise beim Debuggen
ist der, daß man die Teile aufsplittet und getrennt
laufen läßt => Kapselung/Modularisierung.
Man kann die EInzelnen Teile getrennt betrachten und testen
und fügt sie dann zusammen.
Das hat Vorteile beim Debugging.
Threads bieten die Vorteile, daß man recht bequem
Daten austauschen kann, indem man mit globalen Variablen
arbeitet. Das sollte man doch aber ohnehin nicht als die
beste Vorgehensweise betrachten. Globale Variablen bringen
doch eh fast nur Probleme mit sich.
Das Verhältnis von Prozessen und Threads ist doch
ähnlich wie das zwischen Datenkapselung/OO und
konventioneller C-Programmierung, angehäuft
mit globalen Variablen....
Irgendwo ist es schon wiedersprüchlich, OO wegen der Datenkapselung
zu befürworten, dann aber Threads zu nutzen, die Kapselung
in Prozesse aufhebt.
OK, man will ja auch nicht zu dogmatisch damit umgehen
und erlaubt sich beides. Aber wenn ich mir mal die
beispiele, die Butenhof für Thread-Programmierung
anführt, anschaue, dann sehe ich darin keinen großen
Sinn mehr. Warum mit Threadprogrammierung eine Pipe
simulieren, wenn ich ganz einfach eine Pipe erzeugen
und diese nutzen kann? Wegen der Perormance?
Da hat man in der Zeit, die in Entwicklung und insbes. Debugging
großer Applikationen gesteckt wird schon zehnmal schnellere Rechner.
Und man hat mit System-Pipes das verhalten, das man
mit den pipes eben bekommt - deterministisch.
bei selbstimplementiertem Zeugs muß man sich dann durch die
meist schlechten oder garnicht vorhandenen Dokus quälen.
Also auch hier: Nimm Libs => also pipe, statt eine
Pipe selbst wieder zu erfinden, aber den Stempel "Threads inside"
drauf zu haben.
Sicherlich geibt es auch die eine oder andere Anwendung,
wo Threads Sinn machen.
Aber wenn die Apologeten der Threads schon mit beispielen
ankommen, die einem nur Mehraufwand bescheren, dann kann
man das nicht als gelungene Überzeugungsarbeit bezeichnen.
[...]
>> Bei Prozessen bist du dagegen auf bestimmte, exakt umrissene
>> IPC-Möglichkeiten festgelegt. Das hält vielmehr dazu an, die
>> Kommunikation ordentlich zu gestalten, was am Ende der Stabilität und
>> Wartbarkeit zugute kommt.
> Die Moeglichkeiten, die Du mit Threads hast sind eine Obermenge derjenigen,
> die mit Prozessen verfuegbar sind (ich kann mit Threads wenn ich will auch
> Pipes verwenden, nur als Beispiel). Die Frage ist, ob Du beim Entwerfen einer
> Loesung fuer ein bestimmtes Problem von vorneherein bestimmte Loesungs-
> moeglichkeiten ausschliessen willst, weil Deine Umgebung sie nicht moeglich
> macht. Traditionell haben C Programmierer unter Unix solche Einschraenkungen
> eher abgelehnt, die Philosopie war "Gib mir alle Moeglichkeiten, wenn ich mich
> dann in den Fuss schiesse, ist das mein Problem". Das ist fuer mich der Grund,
> warum ich C fuer eine gute Sprache fuer viele Probleme halte, warum ich Unix
> mag, und auch warum ich Threads mag. Sie geben mir viel Freiheit, die aber
> selbstverstaendlich auch missbraucht werden kann. Die Verantwortung, diese
> Freiheiten nicht zu missbrauchen liegt aber bei mir.
Es ist immer schön, die freiheit zu haben.
Zur Freiheit, Möglichkeiten einzusetzen gehört aber auch
die Möglichkeit, sich gegen den Einsatz der Möglichkeiten
auszusprechen, ohne daß man aber die Wahlfreiheit darüber
aufgeben möchte.
Ich finde es auch prima, daß es Thread-libs gibt und die
Möglichkeit, Threads zu nutzen, wenn sie sinnvoll sind.
Aber das bedeutet noch lange nicht, Threads deswegen
zu bevorzugen, nur weil sie mehr Möglichkeiten bieten.
Wenn diese definitiv nicht gebraucht werden, holt
man sich damit nämlich auch die Möglichkeiten, fehler
zu machen, mit an Land.
Keine Threads zu nutzen, bedeutet ja nicht, daß man sie sich
für immer versagt. Aber der Aufwand sollte schon gerechtfertigt sein.
Nun sind wir hier aber in der Unix-Programmier-NG und
da ist es klar, daß Du nicht so viele Thread-Fürsprecher
wie in Windows-NGs finden wirst.
Du willst das gerne auch auf Windows portieren. OK, dann mögen
Threads eine sinnvolle Variante sein.
ich denke, die meisten Leute in der NG haben das eh nicht vor,
für Windows programme zu schreiben.
Damit fällt das Hauptargument weg.
Wozu Threads nutzen, wenn der Mehraufwand nicht unbedingt
gerechtfertigt ist?
Aber ich lasse mich mit dem Beispiel, das ihr beide
hier mal programmieren ollt (wollt ihr doch?) gerne
mal überzeugen...
Die Beispiele, die mir untergekommen sind, waren bisher
noch nicht so überzeugend. Aber vieleicht lohnt sich
der Aufwand ja für Dein Projekt doch.
Also: Am besten mal *machen*.
>> Ich habe noch keine Anwendungen auf Windows portiert, und habe auch
>> nicht vor, diesen Umstand zu ändern. Aber wegen Cygwin bezweifle ich,
>> daß die Prozess-Variante wesentlich länger zur Portierung braucht (eher
>> weniger).
> Das letzte Mal als ich mir Cygwin angeschaut habe war die nowendige DLL 6MB(!)
> gross, hat die ueblichen Versionsprobleme gehabt, und es wurde extra darauf
> hingewiesen, dass fork() zwar in den meisten Faellen funktionieren wuerde,
> dass es aber extrem ineffektiv sei, und man deshalb keine Anforderungen an die
> Performance stellen sollte. Vielleicht hat sich das ja geaendert, fuer mich
> klang es damals auf jeden Fall nicht wie etwas, was ich meinen Kunden
> unbedingt empfehlen sollte.
Na gut, das sind dann aber keine programmierprobleme mehr für die
Unix-NG, sondern sachen, die auf einer anderen Ebene angesiedelt
sind: Läuft der kram auch auf Windows-Kisten vernünftig.
Dafür ist das dann aber hier nicht das passende Diskussionsforum.
Wenn Deine Thread-Variante auch alleine nur im Unix-Umfeld
überzeugend ist, dann erst hast Du mit Deinen Argumenten
gewonnen.
Wie gesagt, ich bin mal an den zwei verschiedenen
Implementierungen interessiert, um mal eine
praxisnahe Sache zum Vergleich zu haben.
>> Mir ist auch klar, daß die Verwaltung einer Send-Queue keinen
>> übermässigen Aufwand bedeutet. Dennoch gilt für mich KISS, auch wenn der
>> Aufwand noch so klein ist, er wird erst gemacht, wenn er sich als
>> absolut notwendig erwiesen hat.
> KISS ist ok, aber auch hier gibt es nicht nur schwarz und weiss.
> Implementierst Du den "--help" Kommandozeilenparameter auch erst dann, wenn
> verzweifelte Anfragen von Benutzern kommen, die sich wundern, warum Dein
> Programm nicht dazu zu ueberreden ist, auch nur den kleinsten Hinweis auf die
> Benutzung zu geben? Das waere KISS in Reinform.
Ja, aber mit solchen dogmatischen Auslegungen brauchst Du nicht ankommen.
Grundlegendes Problem im Bereich der Softwareerstellung ist, daß
man immer zu viele Features implementiert; daß die Dokumentation
auch oft als überflüssiges Feature angehsehen wird, ist fatal.
Lieber etwas mehr doku und weniger Bloat an Funktionen-Schnickschnack.
Lieber erweiterbare Systeme, als tausende einzelner Schnickschnack-
Funktionen. Und lieber weniger Programmieraufwand, als mehr.
Und wenn sich ein Problem mit Prozessen gut lösen läßt, würde ich
diese Lösung einer Thread-Lösung vorziehen.
Wenn es mit Prozessen zu aufwendig wird und Threads Vorteile
bringen würden, dann kann man sich das mit dem Synchronisationsaufwand
auch noch ans Bein binden.
Also, macht doch einfach mal euren kleinen Wettbewerb, dann können
wir mal vergleichen, statt nur herum zu philosophieren.
> In der Realitaet ist es wohl
> immer eine Abwaegung: Welche Vorteile habe ich auf der einen Seite und wie
> gross ist auf der anderen Seite die Chance, dass ich Fehler in den Code mache.
> Insofern gilt Dein "It depends" auch hier:-)
Ja, klar.
>> So wie ich den OP verstanden habe ist sein Protokoll im wesentlichen
>> stateless, von irgendeiner Form von "Sessions" war nicht die Rede.
> Korrekt. Mein Ansatz ist aber normalerweise: Kann ich die Loesung fuer ein
> Problem so allgemein halten, dass ich sie das naechste Mal bei einer
> aehnlichen Aufgabenstellung wiederverwenden kann?
...klar...
Diese Fragestellung ist aber unabhängig davon, ob man nun
Threads oder prozesse nutzt.
Was sind fuer Dich "spezielle" Libs? Mein RedHat kommt mit Bibliotheken, die
bei Slackware, Irix oder Solaris nicht dabei sind (sich aber dort auch
installieren lassen). Sind sie deshalb "speziell" und dürfen bei Aufwands-
schaetzungen fuer Projekte auf RedHat Linux nicht mitgerechnet werden?
Wenn Gnome bei Irix nicht dabei ist, muss ich dann, wenn ich ein Gnome
Programm fuer Irix schreibe so tun, als ob ich alle Gnome Libraries neu
programmieren muesste? Oder darf ich mir die Gnome Libs von freeware.sgi.com
runterladen und dann den zusaetzlichen Aufwand mit nahe 0 ansetzen?
Ich denke Du siehst, dass es so einfach nicht ist.
> Wenn alle die gleichen libs bekommen, also C-Lib, dann
> Unix-API und dazu POSIX-Threads, aber keine speziellen, die
> entweder Prozess- oder Thread-programmierung bevorzugen, dann
> erst kann man die Prozess- und Thread-herangehensweise vergleichen.
Nur dass das dann nicht mehr der Realitaet entspricht.
Ich wehre mich aber garnicht dagegen, den Aufwand der Bibliotheken zu
vergleichen, bzw. zum Vergleich bereitzustellen. Das was ich da habe ist nicht
sonderlich aufwendig, das Thread Modul fuer Unix hat 226 Zeilen (wie bereits
erwaehnt programmiere ich sehr "luftig", das kann man locker auf 150 Zeilen
bringen), bei den Semaphoren sind's knapp 700, wobei nicht alles, was da drin
ist fuer das vorgeschlagene Programm benoetigt wird. Wobei ich natuerlich
nicht mehr sagen kann, wie gross der Aufwand der Erstellung war, das ist zum
einen Jahre her und ist zum zweiten in verschiedene Projekte eingeflossen.
> Das liesse sich evtl. so auch denken.
Fehlt noch die Zusage von Ingo. Oder hat jemand anderes Lust, mal ein paar
Fakten beizusteuern?
Zumindest die glibc kommt mit einer Version von malloc() (dmalloc), die von
sich selber behauptet, auch in multithreaded Umgebungen das notwendige Locking
auf ein Minimum zu reduzieren. Andere Platformen (AIX zum Beispiel) erlauben
es, separate Heaps fuer die einzelnen Threads bereitzustellen, die dann ohne
Locking auskommen - wobei das dann natuerlich nicht mehr portabel ist.
>> Es ist normalerweise einfacher, alles zu terminieren und neu zu starten, als
>> innerhalb vom Programm herauszufinden, welchen Fehler man in das Programm
>> eingebaut hat, und darauf zu reagieren.
>
> Na, das scheint nicht die optimale Debugging-Strategie zu sein,
> das programm zu beenden, wenn es Probleme macht.
Im Gegenteil, das ist die optimale Debugging-Strategie. Erstens kehrt es keine
Fehler unter den Tisch und zweitens erzeugt es (bzw. ich) einen core dump, der
den Programmzustand im Moment des Fehlers wiedergibt. Beides hast Du nicht,
wenn Du versuchst irgendwie um Fehler im Programm selber herumzuwurschteln.
> Wobei oben aber davon gesprochen wurde, daß etwas abraucht.
> Da hast Du dann keine Chance mehr, Dein Programm a) zu beenden
> und b) irgendwo anders zu debuggen, denn der Prozess mit Deinen
> vielen Threads ist gleich mit abgesemmelt.
Wenn das Programm abraucht, dann beendet es sich im Normalfall selber.
Entweder indem es etwas macht, was das OS nicht erlaubt, oder indem es auf
einen der vielen Checkpoints auflaeuft, die ich dort reinsetze, und die
Parameter, Timeouts und aehnliches validieren. In beiden Faellen erzeuge ich
einen core dump, der mir spaeter eine Problemanalyse erlaubt und versuche
durch einen erneuten Programmstart frisch aufzusetzen.
Im Fall von mehreren Prozessen bedeutet abrauchen zwar normalerweise auch
einen core dump, aber wenn der core dump nur ein Symptom war, und der
eigentliche Fehler in einem der Prozesse sitzt, die weiterlaufen, dann habe
ich spaeter keine Moeglichkeit mehr, das zu untersuchen.
> Der Vorteil von der Prozess-Herangehensweise beim Debuggen
> ist der, daß man die Teile aufsplittet und getrennt
> laufen läßt => Kapselung/Modularisierung.
> Man kann die EInzelnen Teile getrennt betrachten und testen
> und fügt sie dann zusammen.
> Das hat Vorteile beim Debugging.
Du kannst und solltest Programme mit Threads genauso modular aufbauen. Message
Passing zwischen den verschiedenen Funktionsmodulen erlaubt eine genauso
modulare Vorgehensweise.
> Threads bieten die Vorteile, daß man recht bequem
> Daten austauschen kann, indem man mit globalen Variablen
> arbeitet. Das sollte man doch aber ohnehin nicht als die
> beste Vorgehensweise betrachten. Globale Variablen bringen
> doch eh fast nur Probleme mit sich.
Globale Daten sind nur auf den ersten Blick ein Vorteil von Threads. In
Wirklichkeit will man diesen Vorteil nicht oder nur nach wirklich sehr
sorgfaeltiger Abwaegung nutzen.
> Irgendwo ist es schon wiedersprüchlich, OO wegen der Datenkapselung
> zu befürworten, dann aber Threads zu nutzen, die Kapselung
> in Prozesse aufhebt.
Das Argument beruht nur darauf, dass Du (und offenbar ein paar andere hier)
Threads mit "moeglichst leicht(fertig)em Zugriff auf globale Daten"
gleichsetzt. Ich habe von Anfang an gesagt, dass man genau das nicht machen
sollte, und habe verschiedene Methoden gezeigt, die einem helfen, ohne
auszukommen. Nochmal:
* Objekte helfen, Daten die zu einem Thread gehoeren zu kapseln, und einen
ungewollten Zugriff zu vermeiden. Wer natuerlich in voller Absicht mit
mehreren Threads auf globale Daten zugreift ist genauso an seinem
Untergang schuld, wie derjenige, der in voller Absicht im 10. Stock aus
dem Fenster springt.
* Message Passing erlaubt es, in sehr vielen Faellen ohne globale Variablen
auszukommen und zugleich die verschiedenen Threads streng modular zu
halten.
> Sicherlich geibt es auch die eine oder andere Anwendung,
> wo Threads Sinn machen.
> Aber wenn die Apologeten der Threads schon mit beispielen
> ankommen, die einem nur Mehraufwand bescheren, dann kann
> man das nicht als gelungene Überzeugungsarbeit bezeichnen.
Ob etwas, und wenn ja was an diesem Argument dran ist, waere am besten zu
sehen, wenn man wirklich mal eine Loesung fuer ein Problem mit Threads und mit
Prozessen/Pipes oder wasauchimmer haette. Ohne konkrete Belege laesst sich
schwer sagen, was mehr Aufwand verursacht, was komplizierter ist, usw.
> Nun sind wir hier aber in der Unix-Programmier-NG und
> da ist es klar, daß Du nicht so viele Thread-Fürsprecher
> wie in Windows-NGs finden wirst.
Warum? Bist Du der Meinung, dass Unix und Threads schlechter zusammenpassen?,
Oder ist die Thread-Implementation unter Windows besser? Oder sind Prozesse
unter Unix so viel besser als unter Windows? Oder liegt es vielleicht sogar an
der "das haben wir schon immer so gemacht und neumodisches Zeugs kommt uns
nicht in's Haus" Einstellung, die manche mitbringen?
> Du willst das gerne auch auf Windows portieren. OK, dann mögen
> Threads eine sinnvolle Variante sein.
> ich denke, die meisten Leute in der NG haben das eh nicht vor,
> für Windows programme zu schreiben.
> Damit fällt das Hauptargument weg.
Woher weisst Du, was die meisten Leute hier wollen? Ich weiss natuerlich was
einige hier mit viel Beleidigungen und ohne viel Inhalt vorbringen. Ich gebe
ausserdem gerne zu, dass diese Leute sehr lautstark sind. Ich schliesse daraus
aber noch lange nicht, dass es die Mehrheit ist. Ausserdem: Wer kann schon
sagen, was morgen ist? Ich hatte einen Fall, bei dem ich mich mit einem
zweifelnden Kunden auf eine Linux Variante einigen konnte, gerade *weil* ich
versprechen konnte, bei Problemen in Nullkommanix auf Windows weiterzumachen.
Portabilitaet kann also durchaus auch helfen, Unix Programme unter die Leute
zu bringen.
> Aber ich lasse mich mit dem Beispiel, das ihr beide
> hier mal programmieren ollt (wollt ihr doch?) gerne
> mal überzeugen...
Ich will (war schliesslich mein Vorschlag). Von Ingo habe ich nichts dazu
gehoert.
Hmm, ja, soweit Ack. Aber andersherum wird ein Schuh draus: die
übrigbleibenden Prozesse sind nach dem Segfault ja (noch) unbeeinflusst.
Damit habe ich dann auch die Möglichkeit, sauber in einen sicheren
Zustand überzugehen, sprich den Service wegen einer potentiellen
Sicherheitslücke terminieren und eine Benachrichtigung versenden. Bei
Threads kann man das natürlich auch versuchen, was dabei rauskommt,
bleibt aber mehr oder weniger dem Zufall überlassen.
Ein anderer Umstand wurde hier auch noch nicht angesprochen: bei Threads
laufen i.d.R. alle Threads mit den gleichen Privilegien (abgesehen von
einigen Implementierungen z.B. unter Linux, bei denen es auch anders
geht). Bei Prozessen dagegen hat man die Möglichkeit, jedem Prozess nur
die Rechte zu geben, die er für seine Aufgabe benötigt. Insbesondere ist
es dadurch möglich, für die komplexeren Teile des Servers, die
naturgemäss anfälliger für Fehler sind, eine Umgebung zu schaffen, die
keine oder kaum Möglichkeiten bietet, das komplette System zu
komprimittieren, wenn ein Exploit erfolgreich ist.
>> Ich habe noch keine Anwendungen auf Windows portiert, und habe auch
>> nicht vor, diesen Umstand zu ändern. Aber wegen Cygwin bezweifle ich,
>> daß die Prozess-Variante wesentlich länger zur Portierung braucht (eher
>> weniger).
> fork() war einst unter Cygwin *extrem* teuer. Keine Ahnung, ob sich
> das geändert hat.
Mag sein, selbst ausprobiert habe ich es noch nicht, und habe es auch
zumindest in näherer Zukunft nicht vor. Meine Informationen beziehe ich
alleine aus der online verfügbaren Doku zu Cygwin.
<nitpick>
Aber das beeinflusst ja nicht den Aufwand, der für die Portierung nötig
ist ;-)
</>
> Du hast bei Threads mehr potentielle *Moeglichkeiten*, dass was schiefgeht und
> bist deshalb mehr darauf angewiesen, dass Dein Code korrekt ist.
Genau das ist der Punkt. Menschen machen nunmal Fehler, und je weniger
potentielle Fehlerquellen man ihnen an die Hand gibt, umso weniger
Fehler werden sie machen.
> Damit war ich
> schon immer einig. Die Frage ist, ob sich diese Moeglichkeiten in echte
> Probleme uebersetzen, oder nicht. Damit das nicht passiert, zumindest nicht in
> einem kritischen Mass habe ich bereits mehrfach Methoden propagiert, die
> helfen, das zu vermeiden.
Ich bevorzuge die Methode, bei der mir der Kernel auf die Finger
schaut, so daß ich einen Grossteil der Fehler garnicht machen _kann_
(oder nur mit unverhältnismässig hohem Aufwand). Ich weiss, daß ich
nicht perfekt bin, und von daher versuche ich, im vorhinein soviele
Fehlerquellen wie möglich auszuschliessen.
Wenn du das anders siehst, ist das deine Sache.
> Es ist normalerweise einfacher, alles zu terminieren und neu zu starten, als
> innerhalb vom Programm herauszufinden, welchen Fehler man in das Programm
> eingebaut hat, und darauf zu reagieren.
Um den gestorbenen Prozess neu zu starten, brauchst du nicht zu wissen,
woran er gestorben ist. Im SIGCHLD-Handler ein Flag setzen, damit die
Hauptschleife bei nächster Gelegenheit einen neuen Child erzeugt (die
Routine dafür hast du ja sowieso, um initial deine Childs aufzusetzen)
und die Überreste des alten wegräumt (was da genau gemacht werden muss,
hängt natürlich von der verwendeten IPC-Methode ab, z.B. Pipes
schliessen, shmem-Slots freigeben etc.).
Zu den Implikationen insbesondere in punkto Sicherheit, die sich aus
einem solchen Vorgehen ergeben, siehe das Parallelposting von Florian
und mein Followup darauf.
> Mein Verstaendnis, was solche Fehler
> angeht ist: Wenn ein Programm fehlerhaft ist, dann kann ich ihm an der Stelle
> auch nicht mehr trauen, wenn es um die Entscheidung geht, was aus welchen
> Gruenden schiefgegangen ist, deshalb muss ich die Fehlerbehebung/ -umgehung um
> mindestens eine Stufe weiter nach oben verlagern. (Das gilt natuerlich nicht
> bei Fehlern, die ausserhalb des Programms liegen, aber um die geht es hier
> nicht).
Bei Threads ja. Bei Prozessen werden die anderen Prozesse aber nicht
unkontrolliert beeinflusst, so daß man hier noch eine reelle Chance hat,
solche Fehler sauber zu behandeln. Ein bisschen aufgeweicht wird das
natürlich durch Shared Memory, aber auch hier ist der mögliche Schaden,
der in anderen Programmen angerichtet werden kann, klar umrissen und
beschränkt sich auf die Daten, die gerade in Arbeit sind. Wenn mit
Pointern gearbeitet wird, die selbst im Shared Memory liegen, besteht
allerdings die Gefahr, daß sich die Memory Corruption des einen
Prozesses in die anderen fortpflanzt.
> Die Moeglichkeiten, die Du mit Threads hast sind eine Obermenge derjenigen,
> die mit Prozessen verfuegbar sind (ich kann mit Threads wenn ich will auch
> Pipes verwenden, nur als Beispiel). Die Frage ist, ob Du beim Entwerfen einer
> Loesung fuer ein bestimmtes Problem von vorneherein bestimmte Loesungs-
> moeglichkeiten ausschliessen willst, weil Deine Umgebung sie nicht moeglich
> macht. Traditionell haben C Programmierer unter Unix solche Einschraenkungen
> eher abgelehnt, die Philosopie war "Gib mir alle Moeglichkeiten, wenn ich mich
> dann in den Fuss schiesse, ist das mein Problem".
Unbestritten, die Wahlmöglichkeit nimmt dir ja auch keiner weg. Nur habe
ich dann natürlich auch die Möglichkeit, den Weg zu nehmen, auf dem am
wenigsten geladene Kanonen auf meine Füsse zielen.
> Das ist fuer mich der Grund,
> warum ich C fuer eine gute Sprache fuer viele Probleme halte, warum ich Unix
> mag, und auch warum ich Threads mag. Sie geben mir viel Freiheit, die aber
> selbstverstaendlich auch missbraucht werden kann. Die Verantwortung, diese
> Freiheiten nicht zu missbrauchen liegt aber bei mir.
Du wählst den Weg, der dir alle Freiheiten bietet, um dich dann
freiwillig auf ein "sicheres" Subset einzuschränken. Ich wähle
freiwillig den Weg, der mir von vornherein nur ein solches "sicheres"
Subset bietet. Wo liegt der fundamentale Unterschied, ausser daß ich
bei meiner Methode nicht Gefahr laufe, die Grenzen dieses Subsets
versehentlich oder aus Faulheit zu überschreiten?
> Das letzte Mal als ich mir Cygwin angeschaut habe war die nowendige DLL 6MB(!)
> gross, hat die ueblichen Versionsprobleme gehabt, und es wurde extra darauf
> hingewiesen, dass fork() zwar in den meisten Faellen funktionieren wuerde,
> dass es aber extrem ineffektiv sei, und man deshalb keine Anforderungen an die
> Performance stellen sollte. Vielleicht hat sich das ja geaendert, fuer mich
> klang es damals auf jeden Fall nicht wie etwas, was ich meinen Kunden
> unbedingt empfehlen sollte.
Mag sein, wie ich schon sagte, ich habe keine praktischen Erfahrungen
mit Cygwin. Aber die Gruppe hat immer noch ein *.unix.* im Namen, von
daher ist das hier zunächst einmal nebensächlich.
> Implementierst Du den "--help" Kommandozeilenparameter auch erst dann, wenn
> verzweifelte Anfragen von Benutzern kommen, die sich wundern, warum Dein
> Programm nicht dazu zu ueberreden ist, auch nur den kleinsten Hinweis auf die
> Benutzung zu geben? Das waere KISS in Reinform. In der Realitaet ist es wohl
> immer eine Abwaegung: Welche Vorteile habe ich auf der einen Seite und wie
> gross ist auf der anderen Seite die Chance, dass ich Fehler in den Code mache.
> Insofern gilt Dein "It depends" auch hier:-)
Ein "--help"[1] oder eine vernünftige Fehlerausgabe sind IMO Dinge, die
implizit immer absolut notwendig sind (sollten sie zumindest sein,
Negativbeispiele gibt es natürlich auch hier). Schliesslich soll das
Endprodukt am Ende von Menschen benutzt werden. Aber KISS ist, daß die
betreffende Option dann immer "--help" (bzw. traditionell eigentlich
"-h") heisst, und nicht auf einmal
"--oh-dear-program-please-give-me-a-hint-on-your-usage".
>> So wie ich den OP verstanden habe ist sein Protokoll im wesentlichen
>> stateless, von irgendeiner Form von "Sessions" war nicht die Rede.
> Korrekt. Mein Ansatz ist aber normalerweise: Kann ich die Loesung fuer ein
> Problem so allgemein halten, dass ich sie das naechste Mal bei einer
> aehnlichen Aufgabenstellung wiederverwenden kann?
Ja, das mache ich auch. Aber wenn dieses dann bedeuten würde, daß der
aktuell benötigte Code merklich komplexer wird, dann lasse ich es
bleiben.
Wenn dann später eine ähnliche Aufgabe ansteht, kann man immer noch den
alten Code soweit verallgemeinern, daß er sich wiederverwenden lässt.
>> Hier ging es aber in erster Linie
>> darum, wie man die eintreffenden Requests am besten entgegennimmt und
>> die Verarbeitung koordiniert, was da im einzelnen verarbeitet wird ist
>> irrelevant, und dabei macht es sehr wohl einen grossen Unterschied, ob
>> man ein TCP- oder ein UDP-basiertes Protokoll hat.
> Was verarbeitet wird ist irrelevant, aber es kommt dabei sehr wohl auf das
> Protokoll an? Das klingt fuer mich wie ein Widerspruch. Es ist wurscht, was
> verarbeitet wird und es ist auch egal, woher es kommt und wie es uebertragen
> wird. Das ist doch der ganze Trick bei der Abstrahierung: Dass ich eben genau
> diese Details aussen vor lasse. Natuerlich hat auch diese Abstraktion Grenzen,
> aber UDP und TCP lassen sich relativ leicht unter einen Hut bringen, warum
> sollte ich mich dann auf eines der beiden festlegen? Wobei die Anmerkungen
> ueber KISS natuerlich auch hierfuer gelten:-)
Wir reden hier nicht von der konkreten Datenverarbeitung, sondern von
der Kommunikation mit dem Client und der internen Kommunikation des
Servers. Dabei ist es natürlich relevant, welche IPC-Mechanismen
verwendet werden und ob UDP oder TCP verwendet wird (da letzteres u.a.
Implikationen für die Kommunikation innerhalb des Servers mit sich
bringt). Ob die Datenverarbeitung nun darin besteht, wie vom OP
angedeutet Crypto etc. zu betreiben oder einfach nur leere Pakete
zurückzuschicken ist dabei egal. Jetzt klarer?
Andreas
[1] Passend dazu: <ahbou=3B8417BE...@eton.powernet.co.uk> ;-)
> Im Fall von mehreren Prozessen bedeutet abrauchen zwar normalerweise auch
> einen core dump, aber wenn der core dump nur ein Symptom war, und der
> eigentliche Fehler in einem der Prozesse sitzt, die weiterlaufen, dann habe
> ich spaeter keine Moeglichkeit mehr, das zu untersuchen.
Es gibt vergleichsweise wenig Ursachen, warum der unerwartete
Tod eines Prozesses auf einen anderen Prozess zurueckzufuehren
ist. Mir faellt da spontan nur SIGPIPE ein, und das impliziert
eigentlich schon, dass da nicht nur ein Prozess abgeraucht ist.
Die Fehlersuche ist in derartigen Faellen mit grosser
Wahrscheinlichkeit einfacher als bei Threads, da hier bei einer
Ursache innerhalb des Prozesses "Fremdeinwirkung" ziemlich sicher
auszuschliessen ist.
Gruss,
Herbert
--
- Oh, Norris. You made a mistake. Mrs. Rutledge didn't want to see me.
- I'm sorry, Sir. I make many mistakes.
-=-=- -=-=-=-=-
Dipl.Ing. Martin "Herbert" Dietze -=-=- The University of Buckingham -=-=-
Noch ein Punkt, der gegen Threads spricht. Bei Threads ist es spätestens
auf MP-Systemen prinzipbedingt nicht möglich, einen sauberen Coredump zu
erzeugen, der wirklich den Zustand zum Zeitpunkt des Fehlers
wiederspiegelt. Während der Kernel auf einer CPU gerade dabei ist, den
Fehler abzufangen und irgendwann /später/ (wobei "später" auch ein sehr
kleiner Zeitraum sein kann) den Coredump zu erzeugen, laufen ja
möglicherweise andere Threads auf anderen CPUs noch unbehelligt weiter
und verändern den Speicher, der dann im Coredump landet. Da der Kernel
dabei natürlich auch keine Critical Sections im Userspace beachtet, kann
es dabei auch vorkommen, daß im Coredump Zwischenzustände auftauchen,
die keiner der Threads jemals zu Gesicht bekommen hat.
Bei Prozessen hast du dagegen garantiert den tatsächlichen Zustand des
Prozesses im Coredump (natürlich wieder modulo Shared Memory).
> Wenn das Programm abraucht, dann beendet es sich im Normalfall selber.
> Entweder indem es etwas macht, was das OS nicht erlaubt, oder indem es auf
> einen der vielen Checkpoints auflaeuft, die ich dort reinsetze, und die
> Parameter, Timeouts und aehnliches validieren. In beiden Faellen erzeuge ich
> einen core dump, der mir spaeter eine Problemanalyse erlaubt und versuche
> durch einen erneuten Programmstart frisch aufzusetzen.
>
> Im Fall von mehreren Prozessen bedeutet abrauchen zwar normalerweise auch
> einen core dump, aber wenn der core dump nur ein Symptom war, und der
> eigentliche Fehler in einem der Prozesse sitzt, die weiterlaufen, dann habe
> ich spaeter keine Moeglichkeit mehr, das zu untersuchen.
Wenn ein Fehler in einem Prozess zu einem Coredump in einem anderen
Prozess führen kann, dann solltest du deine IPC noch einmal überdenken.
Schau dir mal den Aufbau von qmail an, dessen anerkannte Sicherheit
beruht u.a. darauf, daß keiner der (relativ kleinen und überschaubaren)
Programmteile den Daten von anderen Programmteilen auch nur das kleinste
Vertrauen entgegenbringt. Jeder Teil überprüft die erhaltenen Daten
stets selbst, so daß eine etwaige Störung eines Teils sich nicht auf die
anderen ausbreiten kann.
Mit Threads ist ein solcher Aufbau nicht möglich, da es keine klaren
Grenzen zwischen den einzelnen Teilen gibt. Deine Kapselung in
Objekte/Module existiert nur auf Quellcode-Ebene, auf Maschinenebene ist
das bei Threads nur noch ein einziger homogener Brei.
> Globale Daten sind nur auf den ersten Blick ein Vorteil von Threads. In
> Wirklichkeit will man diesen Vorteil nicht oder nur nach wirklich sehr
> sorgfaeltiger Abwaegung nutzen.
Und warum dann überhaupt noch Threads?
> * Objekte helfen, Daten die zu einem Thread gehoeren zu kapseln, und einen
> ungewollten Zugriff zu vermeiden. Wer natuerlich in voller Absicht mit
> mehreren Threads auf globale Daten zugreift ist genauso an seinem
> Untergang schuld, wie derjenige, der in voller Absicht im 10. Stock aus
> dem Fenster springt.
Du redest die ganze Zeit um den heissen Brei herum. Die Frage, die du
bis jetzt noch nicht beantwortet hast, ist die, warum man sich überhaupt erst
die (potentiell gefährlichen) erweiterten Möglichkeiten von Threads
mit ins Boot holen sollte, wenn man sie dann am Ende doch meidet wie der
Teufel das Weihwasser?
Eine andere Frage, die sich mir spontan stellt: wenn du sowieso alles
sauber kapselst, sollte es doch wohl auch ein leichtes sein, deine
"Bibliothek" unter Windows auf Threads aufsetzen zu lassen und unter
Unix auf Prozessen? So würdest du unter Unix nicht die Vorteile von
Prozessen verschenken (und die erweiterten Möglichkeiten von Threads
(IMO hauptsächlich erweiterte Möglichkeiten, Schweinkram zu
veranstalten) nutzt du nach eigenen Aussagen ja nicht), und unter
Windows würde ich das Argument "geht halt nicht besser" dafür, Threads
einzusetzen, durchaus als akzeptabel ansehen.
> * Message Passing erlaubt es, in sehr vielen Faellen ohne globale Variablen
> auszukommen und zugleich die verschiedenen Threads streng modular zu
> halten.
Und wozu braucht man Threads, um Message Passing betreiben zu können?
Gerüchten zufolge soll das sogar gehen, wenn die beteiligten Programme
nichtmal auf demselben Rechner laufen.
>> Nun sind wir hier aber in der Unix-Programmier-NG und
>> da ist es klar, daß Du nicht so viele Thread-Fürsprecher
>> wie in Windows-NGs finden wirst.
> Warum? Bist Du der Meinung, dass Unix und Threads schlechter zusammenpassen?,
> Oder ist die Thread-Implementation unter Windows besser?
Weder noch. Die Prozess-Implementation unter Windows ist derart
unflexibel, daß einem offenbar häufig keine andere Wahl bleibt, als
Threads zu benutzen. Unter Unix kann man dagegen mit Prozessen
mindestens genauso flexibel umgehen wie mit Threads, so daß sich
schlicht nicht die Notwendigkeit ergibt, Threads zu benutzen (mal
abgesehen von Performanceproblemen auf bestimmten
Unix-Implementierungen).
> Oder sind Prozesse
> unter Unix so viel besser als unter Windows?
Ja. Siehe z.B. die Bemerkungen dazu, wie das Cygwin-fork() implementiert
ist.
Eben gerade nachgezählt, CreateProcessA() unter Windows hat mal eben
schlappe 30 Parameter, um wenigstens ein bisschen Flexibilität zu
erreichen. Unix hat dagegen eine handvoll Systemcalls, von denen man nur
jeweils die aufrufen muss, deren Funktionalität man wirklich braucht,
und durch die Trennung von fork() und exec() erreicht man eine geradezu
traumhafte Flexibilität.
Threads sind unter Unix schlichtweg überflüssig, und deshalb werden sie
auch nur wenig benutzt.
>> Du willst das gerne auch auf Windows portieren. OK, dann mögen
>> Threads eine sinnvolle Variante sein.
>> ich denke, die meisten Leute in der NG haben das eh nicht vor,
>> für Windows programme zu schreiben.
>> Damit fällt das Hauptargument weg.
> Woher weisst Du, was die meisten Leute hier wollen?
Das steht in der Charta. Hier geht es um Programmierung unter Unix, die
Portierung auf Windows hat damit nichts zu tun.
> Portabilitaet kann also durchaus auch helfen, Unix Programme unter die Leute
> zu bringen.
Es geht aber nicht darum "Unix Programme unter die Leute zu bringen".
Das ist eine Einstellung, die am besten zu einem Missionar passt.
Zumindest mir geht es darum, eine technisch saubere Lösung abzuliefern,
und in meinen Augen haben da Threads gegenüber Prozessen deutlich
schlechtere Karten.
Und derartige "portable" Programme bleiben stets Bastarde. Entweder sie
sind darauf ausgelegt, eine der unterstützten Plattformen möglichst
optimal auszureizen, was dann aber i.d.R. dazu führt, daß sie die
anderen Plattformen nicht ausreizen kann, oder sie ist auf garkeine
bestimmte Plattform optimiert. Im letzteren Fall bleibt sie dann
meistens aber auf jeder Plattform hinter native optimierten Programmen
zurück.
Als einziger Ausweg aus dem Dilemma bleibt, die Lowlevelparts für jedes
System komplett neu zu schreiben, unter Verzicht auf irgendwelche
Kompatibilitätsbibliotheken. Dabei muss dann auch das Design der
Lowlevel-Teile an die jeweilige Plattform angepasst werden (unter
Beibehaltung der Schnittstellen zu den höheren Layern). Dadurch steigt
aber der Portierungsaufwand stark an, so daß sich das nur für wenige
Projekte rechnet.
Ok, schau dir den Linux Kernel an und verfolge mal einen Monat
die linux-mm-Mailingliste. Oder schau dir einfach mal das Archiv
an. Noch besser ist es, sich gleich mal linux-kernel einen Monat
anzusehen.
Da siehst du die Folgen von threads sehr genau. Der Kernel
arbeitet naemlich sobald er im Kernel-Modus ist (und er auf
globale Daten zugreifen kann) im Prinzip mit Threads.
Andi kennt diese Listen und liest die groessere davon. Es ist
also genauso praktisch wie dein Zeugs und hat NICHTS mit
Geruechten zu tun.
Das der C-Compiler einen da wenigstens in Bezug auf atomare
Operationen unterstuetzen koennte, ist allerdings wahr *seufz*
MfG
Ingo Oeser
--
Threads are like salt, not like pasta. You like salt, I like
salt, we all like salt. But we eat more pasta. --- Larry McVoy
Der Ausweg ist, wo es auch nur irgend geht Message Passing zu
machen. Diesen Mechanismus gibt es wirklich in jedem OS, das ich
bisher gesehen habe.
Die Kunst ist nun, die vom Design benoetigten IPC-Mechanismen
darauf abzubilden. Durch nichts anderes sind die anderen
IPC-Mechanismen enstanden.
Wir wollen grosze Datenmengen kommunizieren
-> SHM und Semaphoren
Wir wollen das von Host zu Host machen
-> Sockets/Netzwerke etc.
Wir wollen nur einen Datenstrom ala Fliessband realisieren
-> Pipes
Wir wollen nur ein paar Bits toggeln
-> Atomare Operationen
Wir wollen Aktivitaeten synchronisieren
-> Semaphoren
Wir wollen auf Objekten onanieren
-> CORBA
Wir wollen einen Call+Parameter kommunizieren
-> RPC
Die Liste laesst sich beliebig fortsetzen und kombinieren, fuer
alles was es so an IPC gibt.
Ich hab bisher noch nichts gefunden, was sich nicht durch
das Prinzip "Message-Passing" in seinen verschiedenen Varianten
ausdruecken laesst. Message-Passing hingegen laesst sich gerade
deswegen durch eben diese ganzen Mechanismen implementieren.
Alles andere ist nur eine Optimierung, die dann evtl. natuerlich
nicht portabel und vor allem die Wurzel allen Uebels ist ;-)
Grusz
Ingo, schon fast weg
--
In der Wunschphantasie vieler Mann-Typen [ist die Frau] unsigned und
operatorvertraeglich. --- Dietz Proepper in dasr