In meinem neuen Team, das ich verwalte, besteht der Großteil unseres Codes aus Plattform-, TCP-Socket- und HTTP-Netzwerkcode. Alles in C ++. Das meiste davon stammt von anderen Entwicklern, die das Team verlassen haben. Die derzeitigen Entwickler im Team sind sehr schlau, aber in Bezug auf die Erfahrung meist jünger.
Unser größtes Problem: Multithread-Bugs. Die meisten unserer Klassenbibliotheken sind unter Verwendung einiger Thread-Pool-Klassen asynchron geschrieben. Methoden für die Klassenbibliotheken reihen häufig lange laufende Takes von einem Thread in den Threadpool ein, und dann werden die Rückrufmethoden dieser Klasse in einem anderen Thread aufgerufen. Infolgedessen gibt es eine Vielzahl von Fehlern in Randfällen, die falsche Threading-Annahmen beinhalten. Dies führt zu subtilen Fehlern, die über das bloße Vorhandensein kritischer Abschnitte und Sperren zum Schutz vor Parallelitätsproblemen hinausgehen.
Was diese Probleme noch schwerer macht, ist, dass die Versuche, sie zu beheben, oft falsch sind. Einige Fehler, die ich beim Versuch des Teams (oder im Legacy-Code selbst) beobachtet habe, umfassen Folgendes:
Häufiger Fehler Nr. 1 - Behebung des Problems der Parallelität durch einfaches Sperren der gemeinsam genutzten Daten, wobei jedoch vergessen wird, was passiert, wenn Methoden nicht in der erwarteten Reihenfolge aufgerufen werden. Hier ist ein sehr einfaches Beispiel:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
Jetzt haben wir einen Fehler, bei dem Shutdown aufgerufen werden könnte, während OnHttpNetworkRequestComplete ausgeführt wird. Ein Tester findet den Fehler, erfasst den Absturzspeicherauszug und weist den Fehler einem Entwickler zu. Er behebt den Fehler wie folgt.
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;
}
Die obige Korrektur sieht gut aus, bis Sie feststellen, dass es einen noch subtileren Randfall gibt. Was passiert, wenn Shutdown aufgerufen wird, bevor OnHttpRequestComplete zurückgerufen wird? Die realen Beispiele, die mein Team hat, sind noch komplexer und die Randfälle sind während des Codeüberprüfungsprozesses noch schwerer zu erkennen.
Häufiger Fehler Nr. 2 - Beheben von Deadlock-Problemen durch blindes Verlassen der Sperre, Abwarten, bis der andere Thread beendet ist, und erneutes Aufrufen der Sperre - jedoch ohne den Fall zu behandeln, dass das Objekt gerade vom anderen Thread aktualisiert wurde!
Allgemeiner Fehler Nr. 3 - Obwohl die Objekte referenziert sind, gibt die Abschaltsequenz ihren Zeiger "frei". Vergisst jedoch zu warten, bis der noch laufende Thread seine Instanz freigegeben hat. Als solches werden Komponenten sauber heruntergefahren, und dann werden falsche oder verspätete Rückrufe für ein Objekt in einem Zustand aufgerufen, in dem keine weiteren Aufrufe erwartet werden.
Es gibt andere Randfälle, aber die Quintessenz lautet:
Multithreading-Programmierung ist selbst für kluge Köpfe eine harte Angelegenheit.
Während ich diese Fehler auffange, diskutiere ich die Fehler mit jedem Entwickler, um eine geeignetere Lösung zu entwickeln. Ich vermute jedoch, dass sie häufig verwirrt sind, wie sie die einzelnen Probleme lösen sollen, da die "richtige" Korrektur eine enorme Menge an Legacy-Code enthält.
Wir werden bald ausliefern und ich bin mir sicher, dass die Patches, die wir anwenden, für die kommende Veröffentlichung verfügbar sein werden. Danach haben wir etwas Zeit, um die Codebasis zu verbessern und gegebenenfalls zu überarbeiten. Wir werden keine Zeit haben, einfach alles neu zu schreiben. Und der Großteil des Codes ist gar nicht so schlecht. Ich bin jedoch bestrebt, Code so umzugestalten, dass Threading-Probleme vollständig vermieden werden können.
Ein Ansatz, über den ich nachdenke, ist dieser. Verfügen Sie für jede wichtige Plattformfunktion über einen eigenen Thread, in dem alle Ereignisse und Netzwerkrückrufe zusammengefasst werden. Ähnlich wie bei COM-Apartment-Threading in Windows mit Verwendung einer Nachrichtenschleife. Lange Blockierungsvorgänge können immer noch an einen Arbeitspool-Thread gesendet werden, der Beendigungs-Callback wird jedoch für den Thread der Komponente aufgerufen. Möglicherweise teilen sich Komponenten sogar den gleichen Thread. Dann können alle im Thread ausgeführten Klassenbibliotheken unter der Annahme einer einzigen Thread-Welt geschrieben werden.
Bevor ich diesen Weg beschreite, bin ich auch sehr interessiert, ob es andere Standardtechniken oder Entwurfsmuster für den Umgang mit Multithread-Problemen gibt. Und ich muss betonen - etwas jenseits eines Buches, das die Grundlagen von Mutexen und Semaphoren beschreibt. Was denkst du?
Ich interessiere mich auch für andere Ansätze für einen Refactoring-Prozess. Einschließlich einer der folgenden:
Literatur oder Papiere über Designmuster um Fäden. Etwas jenseits einer Einführung in Mutexe und Semaphoren. Wir brauchen auch keine massive Parallelität, nur Möglichkeiten, ein Objektmodell so zu entwerfen, dass asynchrone Ereignisse von anderen Threads korrekt verarbeitet werden .
Möglichkeiten, das Threading verschiedener Komponenten grafisch darzustellen, so dass es einfach ist, Lösungen für das Threading zu studieren und zu entwickeln. (Dies ist eine UML-Entsprechung zum Erläutern von Threads über Objekte und Klassen hinweg.)
Informieren Sie Ihr Entwicklungsteam über die Probleme mit Multithread-Code.
Was würdest du tun?