2011-12-21 19 views
8

Sto scrivendo un server multi-threaded POSIX compatibile in c/C++ che deve essere in grado di accettare, leggere e scrivere su un numero elevato di connessioni in modo asincrono. Il server ha diversi thread di lavoro che eseguono attività e, occasionalmente (e in modo imprevedibile), i dati delle code da scrivere nei socket. I dati sono anche occasionalmente (e imprevedibilmente) scritti sui socket dai client, quindi il server deve anche leggere in modo asincrono. Un modo ovvio per fare questo è dare a ciascuna connessione un thread che legge e scrive da/verso il suo socket; questo è brutto, tuttavia, dal momento che ogni connessione può persistere per un lungo periodo di tempo e il server potrebbe quindi dover contenere centinaia o migliaia di thread solo per tenere traccia delle connessioni.In attesa di una condizione (pthread_cond_wait) e un cambio di socket (selezionare) simultaneamente

Un approccio migliore sarebbe avere un singolo thread che gestisse tutte le comunicazioni usando le funzioni select()/pselect(). Ad esempio, un singolo thread attende che qualsiasi socket sia leggibile, quindi genera un lavoro per elaborare l'input che verrà gestito da un pool di altri thread ogni volta che l'input è disponibile. Ogni volta che gli altri thread di lavoro producono output per una connessione, vengono messi in coda e il thread di comunicazione attende che tale socket sia scrivibile prima di scriverlo.

Il problema con questo è che il thread di comunicazione potrebbe essere in attesa nella funzione select() o pselect() quando l'output viene accodato dai thread worker del server. È possibile che, se nessun input arriva per diversi secondi o minuti, un chunk di output in coda attende solo che il thread di comunicazione sia terminato select() ing. Questo non dovrebbe accadere, tuttavia, i dati dovrebbero essere scritti il ​​prima possibile.

In questo momento vedo un paio di soluzioni a questo thread-safe. Uno è quello di avere il thread di comunicazione occupato - attendere l'input e aggiornare l'elenco di socket su cui attende per scrivere ogni decimo di secondo. Questo non è ottimale poiché implica l'attesa, ma funzionerà. Un'altra opzione è quella di usare pselect() e inviare il segnale USR1 (o qualcosa di equivalente) ogni volta che un nuovo output è stato accodato, consentendo al thread di comunicazione di aggiornare immediatamente l'elenco di socket in attesa di uno stato scrivibile. Preferisco quest'ultimo, ma non mi piace usare un segnale per qualcosa che dovrebbe essere una condizione (pthread_cond_t). Un'altra opzione sarebbe quella di includere, nell'elenco dei descrittori di file su cui select() è in attesa, un file fittizio che scriviamo un singolo byte ogni volta che un socket deve essere aggiunto al fd_set scrivibile per select(); ciò riattiverebbe il server di comunicazione perché quel particolare file fittizio sarebbe quindi leggibile, consentendo in tal modo al thread di comunicazione di aggiornare immediatamente il proprio fd_set scrivibile.

Mi sembra intuitivo, che il secondo approccio (con il segnale) è il modo "più corretto" per programmare il server, ma sono curioso se qualcuno sa o quale di questi è il più efficiente, in generale, se uno dei due sopra causerà condizioni di gara di cui non sono a conoscenza, o se qualcuno conosce una soluzione più generale a questo problema. Quello che voglio veramente è una funzione pthread_cond_wait_and_select() che consente al thread di comunicazione di attendere sia una modifica dei socket che un segnale da una condizione.

Grazie in anticipo.

risposta

6

Questo è un problema abbastanza comune.

Una soluzione spesso utilizzata consiste nel disporre di pipe come meccanismo di comunicazione dai thread di lavoro al thread di I/O. Completata la sua attività, un thread di lavoro scrive il puntatore sul risultato nella pipe. Il thread I/O attende sull'estremità di lettura del pipe insieme ad altri socket e descrittori di file e una volta che il pipe è pronto per la lettura si riattiva, recupera il puntatore sul risultato e procede spingendo il risultato nella connessione client in non -modalità di blocco.

Nota: dato che le letture e le scritture di pipe di dimensioni inferiori o uguali a PIPE_BUF sono atomiche, i puntatori vengono scritti e letti in un'unica operazione. Si può persino avere più thread di lavoro che scrivono i puntatori nella stessa pipe a causa della garanzia di atomicità.

3

Il tuo secondo approccio è la strada più pulita da percorrere. È normale che cose come select o epoll includano eventi personalizzati nel tuo elenco. Questo è ciò che facciamo sul mio attuale progetto per gestire tali eventi. Usiamo anche i timer (su Linux timerfd_create) per eventi periodici.

Su Linux il eventfd consente di creare tali eventi utente arbitrari per questo scopo, quindi direi che è una pratica abbastanza accettata. Per POSIX solo le funzioni, beh, hmm, forse uno dei comandi pipe o socketpair ho visto anche io.

Il polling impegnativo non è una buona opzione.Per prima cosa analizzerete la memoria che verrà utilizzata da altri thread, causando così contesa sulla memoria della CPU. In secondo luogo dovrai sempre tornare alla tua chiamata select che creerà un numero enorme di chiamate di sistema e interruttori di contesto che danneggeranno le prestazioni generali del sistema.

3

Sfortunatamente, il modo migliore per farlo è diverso per ogni piattaforma. Il modo canonico e portatile per farlo è di avere il blocco del thread I/O in poll. Se è necessario ottenere il thread I/O per lasciare poll, si invia un singolo byte su un pipe che il thread sta eseguendo il polling. Ciò causerà l'uscita del thread immediatamente da poll.

Su Linux, epoll è il modo migliore. Sui sistemi operativi derivati ​​da BSD (incluso OSX, penso), kqueue. Su Solaris, era /dev/poll e ora c'è qualcos'altro di cui ho dimenticato il nome.

Si consiglia di prendere in considerazione l'utilizzo di una libreria come libevent o Boost.Asio. Ti danno il miglior modello I/O su ogni piattaforma che supportano.