PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : pthread mem. overhead, bzw. select() vs. pthreads in Serveranwendungen



panzi
22-04-2004, 23:11
Hi

Bin total neu in Netzwerkprogrammierung. Hab' schon einen kleinen socket basierten client und server geschrieben (plus nen http1.0 "client"). Aber nur ganz minimales Zeug. Hab am Server pthreads verwendet, aber ein Freund (der ein recht guter Programmierer ist und grade Informatik Studiert) von mir sagte: (p)threads haben viel zuviel mem.overhead, da ist bei vielen Clients nach kurzer Zeit der RAM voll.

Nun, er ist zwar ein spitzen Programmierer, aber Netzwerkprogrammierung macht er auch noch nicht lange.
Also ich frag' mich ob pthreads tatsächlich sooo viel overhead erzeugen, dass es sinnvoller währe mit select() zu arbeiten. Ich mein bei select() bühst man einiges an Parallelität ein.

Also gibt es eine Funktion, mit welcher ich den aktuellen Speicherverbrauch des aufrufenden Programmes (inkl. aller pthreads) abfragen kann? Damit ich mir mal anschaun kann wieviel pthreads wirklich verbrauchen. (Valgrind möcht ich mir jetzt nicht installieren und mich da erst einlernen.) Ansonsten müsst ich mit /proc/<pid>/* arbeiten...

amiga
23-04-2004, 10:47
Hi :)

ich arbeite gerade an einem HTTP basierten Chatserver (PS: templateLanguage und parseParams funktionieren jetzt einwandfrei, danke nochmal für deinen Tipp :D)

Meiner Meinung nach lassen sich Threads kaum vermeiden, wenn viele Anfragen gleichzeitig abgearbeitet werden müssen. Es kann z.B. immer mal vorkommen, dass ein gethostbyaddr Aufruf mal länger dauert, z.B. wenn externe Nameserver in der /etc/resolv.conf stehen, oder wenn mehrere Nameserver gefragt werden müssen. Ein non-threaded Server würde dann hängen und während dieser Zeit keine Anfragen annehmen können.

Deshalb hab ich's bei mir so gemacht, dass ich im mainThread select() aufrufe und auf eingehende Verbindungen warte.
Kommt eine Verbindungsanfrage, wird ein pthread erstellt (mit dem Attribut detached, so dass man nicht mit pthread_join auf das Ende warten muss). Der Thread kümmert sich dann um accept und alles Andere.

Ich könnte mir vorstellen, dass dein Freund da die Situation mit fork() verwechselt. Da ist der Memory Overhead wirklich extrem, da der gesamte Adressraum und Filedeskriptoren des Parents dupliziert werden, so dass Parent und Child als völlig unabhängige Prozesse ablaufen.

Pthreads teilen sich dagegen den Addressraum und die Filedeskriptoren (unter Linux ist pthread_create mit clone()-Aufrufen implementiert).

In einem Stresstest haben wir in unserer Chatserver-Anwendung problemlos 1000 User simulieren können. Für jeden User müssen wir eine Socket-Verbindung für den Chatstream offenhalten. Da Pthreads sich die Filedeskriptoren teilen, müssen wir auch nicht für jede Verbindung einen Pthread offen halten. Wir können den Pthread beenden ohne dass die Verbindung beendet wird. Aufräumarbeiten werden von einem Thread erledigt, der mit pthread_cond_wait auf "User-Verlässt-Chat"-Signale wartet.

Das Problem ist nur, dass irgendwann die Filedeskriptoren knapp werden könnten ;) dann hat man eigentlich keine andere wahl, als einen neuen Prozess als FileDescriptor-Container zu forken ;) bis jetzt sind wir aber immer mit den FDs ausgekommen :)

Bei IBM gibt es eine gute Dokumentation über Posix Threads, geschrieben von Daniel Robbins, President/CEO Gentoo Technologies, Inc. :
http://www-106.ibm.com/developerworks/library/l-posix1/

Er schreibt :

... Threads also happen to be extremely nimble. Compared to a standard fork(), they carry a lot less overhead. The kernel does not need to make a new independent copy of the process memory space, file descriptors, etc. That saves a lot of CPU time, making thread creation ten to a hundred times faster than new process creation. Because of this, you can use a whole bunch of threads and not worry too much about the CPU and memory overhead incurred. You don't have a big CPU hit the way you do with fork(). This means you can generally create threads whenever it makes sense in your program.

peschmae
23-04-2004, 10:57
Könntest ja mal gucken wie Apache arbeitet. Ich glaub da gibts mittlerweile eine recht breite Palette an Möglichkeiten (viele Prozesse, beschränkte Prozesszahl, Threads,...) sowas zu machen.
Ich würde mal schauen ob du irgendwo ein Benchmark oder so kriegst das erzählt, was da am effektivsten ist.

MfG Peschmä

panzi
23-04-2004, 16:25
Original geschrieben von amiga
[...] Wir können den Pthread beenden ohne dass die Verbindung beendet wird. Aufräumarbeiten werden von einem Thread erledigt, der mit pthread_cond_wait auf "User-Verlässt-Chat"-Signale wartet.[...]

So in der Art hatt ich's mir auch gedacht. ;) (mit nen garbage collecting thread, dem die anderen threads sagen wenn sie sterben)

[QUOTE]Original geschrieben von amiga
[...]Das Problem ist nur, dass irgendwann die Filedeskriptoren knapp werden könnten ;) dann hat man eigentlich keine andere wahl, als einen neuen Prozess als FileDescriptor-Container zu forken ;) bis jetzt sind wir aber immer mit den FDs ausgekommen :)[...][/QUOTE

Ich glaub der Fall ist mir mal wurscht. ;)

Nur ein problem gibt's, für das ich eine (halbwegs unschöne) Lösung hab:
Die Struktur fd_set, welche von select() gebraucht wird, hat per default eine begrenzung auf 64 FDs. Man kann mit nen Makro eine größere Anzahl definieren, aber das ist alles irgendwie sehr undynamisch. Denn wieviel nehm ich? Will das meine fd_set Struktur "atmen" kann.

Hab mir dazu ne Struktur xfd_set überlegt, die man auch per dem Makro xfd_set_fd_set(xfd_set*) zu fd_set* casten kann.
Für diese Struktur hab ich Funktionen geschrieben, die das Array dynamisch mit realloc() u.d.G. anpassen kann. (Boundaries werden natürlich auch gechecked.)

Hab's noch nicht getestet / ausprobiert, aber hier ist mal der vorläufige Code:

xfd_set.h
#include <sys/param.h>
#include <sys/types.h>

typedef unsigned short masksize_t;

/* xfd_set_data can be casted to fd_set
without any problems, I hope... */
typedef struct xfd_set_data_s {
fd_mask * fd_bits;
} xfd_set_data;

typedef struct xfd_set_s {
xfd_set_data data;
masksize_t masks;
} xfd_set;

/* usage:
======

xfd_set set;
struct timeval timeout;

xfd_set_init( &set );

timeout.tv_sec = 5;
timeout.tv_usec = 0;

// <verion_1>

xfd_set_set( &set, stdin );
// ...
// call xfd_set_resize() when xfd_set_set() == EINVAL
// check for: xfd_set_resize() == ENOMEM !!

if( select( xfd_set_maxfd( &set ) + 1, xfd_set_fd_set( &set ), NULL, NULL, &timeout ) < 0 ) {
// handle error ...
}
else {
// read ...
}

// </version_1>
// <verion_2>

int maxfd = -1;

xfd_set_setmax( &set, stdin, &maxfd );
// ...
// call xfd_set_resize() when xfd_set_setmax() == EINVAL
// check for: xfd_set_resize() == ENOMEM !!

if( select( maxfd + 1, xfd_set_fd_set( &set ), NULL, NULL, &timeout ) < 0 ) {
// handle error ...
}
else {
// read ...
}

// </version_2>
// <verion_3>

int maxfd = -1;

xfd_set_xsetmax( &set, stdin, &maxfd );
// ...
// check for: xfd_set_xsetmax() == ENOMEM !!
// no xfd_set_resize() is required here.

if( select( maxfd + 1, xfd_set_fd_set( &set ), NULL, NULL, &timeout ) < 0 ) {
// handle error ...
}
else {
// read ...
}

// </version_3>

xfd_set_destroy( &set );
*/

/*
TODO:
Überlegen, ob ich die Funktionen, die sowieso immer 0 retunieren, nicht void mache:
xfd_set_destroy(), xfd_set_clearall()
*/

/* return values: */
int xfd_set_init ( xfd_set * set ); /* 0, ENOMEM */
int xfd_set_destroy ( xfd_set * set ); /* 0 */
int xfd_set_set ( xfd_set * set, int fd ); /* 0, EINVAL */
int xfd_set_setmax ( xfd_set * set, int fd, int * maxfd ); /* 0, EINVAL */
int xfd_set_xset ( xfd_set * set, int fd ); /* 0, ENOMEM */
int xfd_set_xsetmax ( xfd_set * set, int fd, int * maxfd ); /* 0, ENOMEM */
int xfd_set_clear ( xfd_set * set, int fd ); /* 0, EINVAL */
int xfd_set_clearall( xfd_set * set ); /* 0 */
int xfd_set_isset ( const xfd_set * set, int fd ); /* 0, 1 */
int xfd_set_fdmax ( const xfd_set * set ); /* fdmax */
int xfd_set_copy ( const xfd_set * set, xfd_set * dest ); /* 0, ENOMEM */
int xfd_set_resize ( xfd_set * set, masksize_t masks ); /* 0, ENOMEM */
int xfd_set_pack ( xfd_set * set ); /* 0, ENOMEM */

#define xfd_set_fd_set( set ) ((fd_set*)&((set)->data))
#define xfd_set_inrange( set, fd ) (((fd)/NFDBITS) < (set)->masks)

xfd_set.c
#include "xfd_set.h"

int xfd_set_init( xfd_set * set ) {
size_t memsize = (set->masks = howmany(FD_SETSIZE, NFDBITS)) * sizeof(fd_mask);

set->data.fd_bits = malloc( memsize );

if( set.data->fd_bits ) {
memset( set->data.fd_bits, 0, memsize );

return 0;
}
else {
return ENOMEM;
}
}

int xfd_set_destroy( xfd_set * set ) {
free( set->data.fd_bits );
/*
set->data.fd_bits = NULL;
set->masks = 0;
*/
return 0;
}

int xfd_set_set( xfd_set * set, int fd ) {
if( xfd_set_inrange( set, fd ) ) {
FD_SET( fd, xfd_set_fd_set( set ) );
return 0;
}
else {
return EINVAL;
}
}

int xfd_set_setmax( xfd_set * set, int fd, int * maxfd ) {
int errnum = xfd_set_set( set, fd );

if( errnum == 0 ) {
*maxfd = MAX( *maxfd, fd );
}

return errnum;
}

int xfd_set_xset( xfd_set * set, int fd ) {
if( xfd_set_inrange( set, fd ) ) {
FD_SET( fd, xfd_set_fd_set( set ) );
return 0;
}
else {
int errnum = xfd_set_resize( set, set->masks + 1 );

if( errnum == 0 ) {
FD_SET( fd, xfd_set_fd_set( set ) );
}
return errnum;
}
}

int xfd_set_xsetmax( xfd_set * set, int fd, int * maxfd ) {
int errnum = xfd_set_xset( set, fd );

if( errnum == 0 ) {
*maxfd = MAX( *maxfd, fd );
}

return errnum;
}

int xfd_set_clear( xfd_set * set, int fd ) {
if( xfd_set_inrange( set, fd ) ) {
FD_CLR( fd, xfd_set_fd_set( set ) );
return 0;
}
else {
return EINVAL;
}
}

int xfd_set_clearall( xfd_set * set ) {
memset( set->data.fd_bits, 0, set->masks * sizeof(fd_mask) );
return 0;
}

int xfd_set_isset( const xfd_set * set, int fd ) {
if( xfd_set_inrange( set, fd ) ) {
return FD_ISSET( fd, xfd_set_fd_set( set ) );
}
else {
return 0;
}
}

/* TODO: Test this! */
int xfd_set_fdmax( const xfd_set * set ) {
int fdmax = -1;
masksize_t i = set->masks;

while( i > 0 ) {
if( set->data.fd_bits[ -- i ] )
break;
}

if( i > 0 ) {
fd_mask mask = set->data.fd_bits[ i ];
fdmax = NFDBITS * i - 1;

while( mask ) {
mask >>= 1;
++ fdmax;
}
}

return fdmax;
}

int xfd_set_copy( const xfd_set * set, xfd_set * dest ) {
size_t memsize = set->masks * sizeof(fd_mask);
fd_mask * fd_bits = malloc( memsize );

if( fd_bits ) {
dest->masks = set->masks;
memcpy( dest->data.fd_bits, set->data.fd_bits, memsize );

return 0;
}
else {
return ENOMEM;
}
}

int xfd_set_resize( xfd_set * set, masksize_t masks ) {
fd_mask * fd_bits = realloc( set->data.fd_bits, masks * sizeof(fd_mask) );

if( fd_bits ) {
if( masks > set->masks ) {
memset( &fd_bits[ set->masks ], 0, masks - set->masks );
}

set->data.fd_bits = fd_bits;
set->masks = masks;

return 0;
}
else {
return ENOMEM;
}
}

int xfd_set_pack( xfd_set * set ) {
masksize_t masks = set->masks;
fd_mask * fd_bits = NULL;

while( masks > 0 ) {
if( set->data.fd_bits[ -- masks ] )
break;
}

fd_bits = realloc( set->data.fd_bits, (++ masks) * sizeof(fd_mask) );

if( fd_bits ) {
set->data.fd_bits = fd_bits;
set->masks = masks;
return 0;
}
else {
return ENOMEM;
}
}

panzi
23-04-2004, 20:28
Ok, der gepostete xfd_set code ist nicht korrekt. Hab's jetzt ausgebessert. Die korrekten Datein sind im Anhang.

foobarflu
24-04-2004, 09:04
Die Struktur fd_set, welche von select() gebraucht wird, hat per default eine begrenzung auf 64 FDs

Drum nimmt man poll oder die neueren Varianten. Komfortabel das beste kriegt man mit thttpds (http://acme.com/software/thttpd/) fd_set Makros