Nel mio nuovo team che gestisco, la maggior parte del nostro codice è costituito da piattaforma, socket TCP e codice di rete http. Tutto C ++. La maggior parte proviene da altri sviluppatori che hanno lasciato il team. Gli attuali sviluppatori del team sono molto intelligenti, ma per lo più junior in termini di esperienza.
Il nostro problema più grande: bug di concorrenza multi-thread. La maggior parte delle nostre librerie di classi sono scritte in modo asincrono mediante l'uso di alcune classi di pool di thread. I metodi sulle librerie di classi spesso accodano taks di lunga durata nel pool di thread da un thread e quindi i metodi di callback di quella classe vengono richiamati su un thread diverso. Di conseguenza, abbiamo molti bug di edge case che implicano ipotesi di threading errate. Ciò si traduce in bug sottili che vanno oltre la semplice presenza di sezioni e blocchi critici per evitare problemi di concorrenza.
Ciò che rende ancora più difficili questi problemi è che i tentativi di risoluzione sono spesso errati. Alcuni errori che ho riscontrato nel tentativo del team (o all'interno del codice legacy stesso) includono qualcosa di simile al seguente:
Errore comune n. 1 - Risolvere il problema di concorrenza semplicemente bloccando i dati condivisi, ma dimenticando cosa succede quando i metodi non vengono chiamati in un ordine previsto. Ecco un esempio molto semplice:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Quindi ora abbiamo un bug in cui Shutdown potrebbe essere chiamato mentre OnHttpNetworkRequestComplete si sta verificando. Un tester trova il bug, acquisisce il crash dump e assegna il bug a uno sviluppatore. A sua volta risolve il bug in questo modo.
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
La correzione di cui sopra sembra buona finché non ti rendi conto che c'è un caso ancora più sottile. Cosa succede se Shutdown viene chiamato prima che OnHttpRequestComplete venga richiamato? Gli esempi del mondo reale del mio team sono ancora più complessi e i casi limite sono ancora più difficili da individuare durante il processo di revisione del codice.
Errore comune n. 2 : risolvere i problemi di deadlock uscendo alla cieca dal blocco, attendere il completamento dell'altro thread, quindi immettere nuovamente il blocco, ma senza gestire il caso in cui l'oggetto è stato appena aggiornato dall'altro thread!
Errore comune n. 3 - Anche se gli oggetti vengono contati come riferimento, la sequenza di arresto "rilascia" è puntatore. Ma dimentica di aspettare che il thread ancora in esecuzione rilasci la sua istanza. Pertanto, i componenti vengono arrestati in modo pulito, quindi vengono richiamati richiami spuri o in ritardo su un oggetto in uno stato che non prevede ulteriori chiamate.
Esistono altri casi limite, ma la linea di fondo è questa:
La programmazione multithread è semplicemente dura, anche per le persone intelligenti.
Mentre colgo questi errori, passo il tempo a discutere degli errori con ogni sviluppatore nello sviluppo di una soluzione più appropriata. Ma sospetto che siano spesso confusi su come risolvere ogni problema a causa dell'enorme quantità di codice legacy che la correzione "giusta" comporterà il contatto.
Presto spediremo e sono sicuro che le patch che applicheremo saranno valide per la prossima versione. Successivamente, avremo del tempo per migliorare la base di codice e il refactor dove necessario. Non avremo il tempo di riscrivere tutto. E la maggior parte del codice non è poi così male. Ma sto cercando di refactificare il codice in modo tale da evitare del tutto i problemi di threading.
Un approccio che sto prendendo in considerazione è questo. Per ogni significativa funzionalità della piattaforma, disporre di un singolo thread dedicato in cui vengono raggruppati tutti gli eventi e le richiamate di rete. Simile al threading apartment COM in Windows con l'uso di un loop di messaggi. Le operazioni di blocco lunghe potrebbero comunque essere inviate a un thread del pool di lavoro, ma il callback di completamento viene richiamato sul thread del componente. I componenti potrebbero anche condividere lo stesso thread. Quindi tutte le librerie di classi in esecuzione all'interno del thread possono essere scritte partendo dal presupposto di un singolo mondo thread.
Prima di intraprendere questa strada, sono anche molto interessato alla presenza di altre tecniche standard o modelli di progettazione per affrontare problemi multithread. E devo sottolineare - qualcosa al di là di un libro che descrive le basi dei mutex e dei semafori. Cosa pensi?
Sono anche interessato a qualsiasi altro approccio da adottare per un processo di refactoring. Incluso uno dei seguenti:
Letteratura o documenti sui modelli di progettazione intorno ai fili. Qualcosa oltre un'introduzione a mutex e semafori. Non abbiamo nemmeno bisogno di un parallelismo massiccio, ma solo modi per progettare un modello a oggetti in modo da gestire correttamente eventi asincroni da altri thread .
Modi per tracciare il diagramma della filettatura di vari componenti, in modo che sia facile studiare e sviluppare soluzioni per. (Cioè, un equivalente UML per discutere discussioni tra oggetti e classi)
Educare il team di sviluppo sui problemi con il codice multithread.
Cosa faresti?