Nel mio nuovo team che gestisco, la maggior parte del nostro codice è piattaforma, socket TCP e codice di rete http. Tutto il C ++. La maggior parte proveniva da altri sviluppatori che hanno lasciato la squadra. 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 per essere asincrone mediante l'uso di alcune classi di thread pool. I metodi sulle librerie di classi accodano spesso tak di esecuzione lunghi 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 casi limite che implicano ipotesi di threading non corrette. Ciò si traduce in bug sottili che vanno oltre le semplici sezioni e blocchi critici per evitare problemi di concorrenza.
Ciò che rende questi problemi ancora più difficili è che i tentativi di correzione sono spesso errati. Alcuni errori che ho osservato 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 mettendo un blocco attorno ai dati condivisi, ma dimenticando ciò che accade quando i metodi non vengono richiamati 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, cattura 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 sopra sembra buona fino a quando non ti accorgi che c'è un caso limite ancora più sottile. Cosa succede se Shutdown viene chiamato prima OnHttpRequestComplete viene richiamato? Gli esempi reali del mio team sono ancora più complessi e i casi limite sono ancora più difficili da individuare durante il processo di revisione del codice.
Common Error # 2 - risolvendo i problemi di deadlock chiudendo ciecamente il lock, aspettando che l'altro thread finisca, quindi reinserendo il lock - ma senza gestire il caso che l'oggetto è stato appena aggiornato dall'altra discussione!
Common Error # 3 - Anche se gli oggetti sono conteggiati, la sequenza di shutdown "rilascia" il suo puntatore. Ma dimentica di aspettare il thread ancora in esecuzione per rilasciare la sua istanza. Pertanto, i componenti vengono arrestati in modo pulito, quindi le callback spurie o tardive vengono richiamate su un oggetto in uno stato che non si aspetta altre chiamate.
Ci sono altri casi limite, ma la linea di fondo è questa:
La programmazione con multithreading è molto semplice, anche per le persone intelligenti.
Mentre percepisco questi errori, passo il tempo a discutere degli errori con ciascuno sviluppatore nello sviluppo di una correzione più appropriata. Ma sospetto che siano spesso confusi su come risolvere ciascun problema a causa dell'enorme quantità di codice legacy che la correzione "giusta" implicherà toccare.
Stiamo per essere spediti presto, e sono sicuro che le patch che applicheremo saranno valide per la prossima versione. In seguito, avremo un po 'di tempo per migliorare il codice base e il refactator dove necessario. Non avremo tempo di riscrivere tutto. E la maggior parte del codice non è poi così male. Ma sto cercando di codice refactoring tale che i problemi di threading possono essere evitati del tutto.
Un approccio che sto considerando è questo. Per ogni funzione di piattaforma significativa, avere un singolo thread dedicato in cui vengono richiamati tutti gli eventi e le chiamate di rete. Simile al threading degli apartment COM in Windows con l'uso di un loop di messaggi. Le lunghe operazioni di blocco possono ancora essere inviate a un thread del pool di lavoro, ma il callback del completamento viene richiamato sulla 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 con l'assunzione di un singolo mondo con thread.
Prima di intraprendere questa strada, sono anche molto interessato se ci sono altre tecniche standard o schemi di progettazione per affrontare problemi con multithreading. E devo sottolineare - qualcosa al di là di un libro che descrive le basi dei mutex e dei semafori. Cosa ne pensi?
Sono anche interessato a qualsiasi altro approccio da adottare nei confronti di un processo di refactoring. Compreso uno dei seguenti:
-
Letteratura o articoli sui modelli di progettazione attorno ai fili. Qualcosa al di là di un'introduzione ai mutex e ai semafori. Non abbiamo nemmeno bisogno di un parallelismo massiccio, solo modi per progettare un modello di oggetto in modo da gestire eventi asincroni da altri thread correttamente .
-
Modi per diagrammare il threading di vari componenti, in modo che sia facile studiare e sviluppare soluzioni per. (Ovvero, un equivalente UML per discutere i thread tra oggetti e classi)
-
Formazione del team di sviluppo sui problemi relativi al codice multithread.
-
Che cosa faresti?