Come segnalare select () per tornare immediatamente?

Ho un thread di lavoro che sta ascoltando un socket TCP per il traffico in entrata, e il buffering dei dati ricevuti per il thread principale per accedere (chiamiamo questo socket A ). Tuttavia, il thread worker deve anche eseguire alcune operazioni regolari (ad esempio una volta al secondo), anche se non ci sono dati in arrivo. Pertanto, utilizzo select() con un timeout, in modo che non sia necessario continuare a eseguire il polling . (Si noti che chiamare receive() su un socket non bloccante e poi dormire per un secondo non è buono: i dati in arrivo dovrebbero essere immediatamente disponibili per il thread principale, anche se il thread principale potrebbe non essere sempre in grado di elaborarlo subito , quindi la necessità di buffering.)

Ora, devo anche essere in grado di segnalare al thread worker di fare immediatamente altre cose; dal thread principale, ho bisogno di rendere il select() del worker thread immediatamente. Per ora, ho risolto questo come segue (approccio sostanzialmente adottato da qui e qui ):

All’avvio del programma, il thread worker crea per questo scopo un socket aggiuntivo del tipo datagramma (UDP) e lo lega ad una porta casuale (chiamiamolo socket B ). Allo stesso modo, il thread principale crea un socket di datagramma per l’invio. Nella sua chiamata a select() , il thread worker ora elenca sia A che B in fd_set . Quando il thread principale deve segnalare, sendto() ‘sa un paio di byte alla porta corrispondente su localhost . Tornando al thread di lavoro, se B rimane in fd_set dopo select() restituisce, allora recvfrom() viene chiamato e i byte ricevuti vengono semplicemente ignorati.

Questo sembra funzionare molto bene, ma non posso dire che mi piaccia la soluzione, soprattutto perché richiede il binding di una porta aggiuntiva per B , e anche perché aggiunge parecchie chiamate API aggiuntive che potrebbero fallire credo – e io non sento davvero di capire l’azione appropriata per ciascuno dei casi.

Penso che idealmente, mi piacerebbe chiamare una funzione che prenda A come input, e non fa altro che fa select() restituire subito. Tuttavia, non conosco tale funzione. (Suppongo che potrei per esempio shutdown() il socket, ma gli effetti collaterali non sono realmente accettabili 🙂

Se ciò non è ansible, la seconda opzione migliore sarebbe quella di creare un B che sia molto più maneggevole di un vero socket UDP, e in realtà non richiede l’allocazione di risorse limitate (oltre una ragionevole quantità di memoria). Immagino che i socket di dominio Unix farebbero esattamente questo, ma: la soluzione non dovrebbe essere molto meno multipiattaforma di quella che ho attualmente, anche se una certa quantità moderata di roba #ifdef va bene. (Mi occupo principalmente di Windows e Linux, oltre a scrivere C ++ a proposito).

Si prega di non suggerire il refactoring per eliminare i due thread separati. Questo design è necessario perché il thread principale può essere bloccato per lunghi periodi (ad esempio, facendo un calcolo intensivo – e non posso iniziare a chiamare periodicamente receive() dal ciclo più interno di calcolo), e nel frattempo, qualcuno deve bufferizzare i dati in arrivo (e per ragioni che vanno al di là di ciò che posso controllare, non può essere il mittente).

Ora che stavo scrivendo questo, mi sono reso conto che qualcuno avrebbe sicuramente risposto semplicemente ” Boost.Asio “, quindi ho appena avuto il mio primo sguardo … Non ho trovato una soluzione ovvia, però. Nota che non posso (facilmente) influire sul modo in cui viene creato il socket A , ma dovrei essere in grado di lasciare che altri oggetti lo avvolgano, se necessario.

Ci sei quasi. Usa un trucco “self-pipe” . Apri una pipe, aggiungila alla select() leggi e scrivi fd_set , fd_set dal thread principale per sbloccare un thread worker. È portatile su tutti i sistemi POSIX.

Ho visto una variante di tecnica simile per Windows in un sistema (in effetti utilizzato insieme al metodo sopra, separato da #ifdef WIN32 ). Lo sblocco può essere ottenuto aggiungendo un socket datagramma fittizio (non fd_set ) a fd_set e quindi chiudendolo. Lo svantaggio è che, naturalmente, devi riaprirlo ogni volta.

Tuttavia, nel suddetto sistema, entrambi questi metodi sono usati piuttosto con parsimonia, e per eventi inaspettati (ad es. Segnali, richieste di terminazione). Il metodo preferito è ancora un timeout variabile per select() , a seconda di quanto presto è programmato un thread di lavoro.

Usare un tubo piuttosto che uno zoccolo è un po ‘più pulito, in quanto non vi è alcuna possibilità che un altro processo possa impadronirsene e rovinare le cose.

L’uso di una presa UDP crea sicuramente il potenziale per i pacchetti vaganti di entrare e interferire.

Una pipe anonima non sarà mai disponibile per nessun altro processo (a meno che tu non glielo consegni).

Potresti anche usare i segnali, ma in un programma multithread devi assicurarti che tutti i thread tranne quello che vuoi abbiano quel segnale mascherato.

Su Unix sarà semplice usare una pipe. Se sei su Windows e vuoi continuare a utilizzare l’istruzione select per mantenere il tuo codice compatibile con unix, il trucco per creare un socket UDP non associato e chiuderlo, funziona bene e facilmente. Ma devi renderlo multi-thread sicuro.

L’unico modo che ho trovato per rendere questo multi-threadsafe è quello di chiudere e ricreare il socket nello stesso thread in cui è in esecuzione l’istruzione select. Ovviamente questo è difficile se il thread sta bloccando la selezione. E poi arriva nella chiamata di Windows QueueUserAPC. Quando Windows si blocca nell’istruzione select, il thread può gestire le chiamate asincrone di procedura. È ansible pianificare questo da un thread diverso utilizzando QueueUserAPC. Windows interrompe la selezione, esegue la funzione nello stesso thread e continua con l’istruzione select. Ora puoi nel tuo metodo APC chiudere il socket e ricrearlo. Filetto garantito sicuro e non perderai mai un segnale.