Von Multithread-Bugs geplagt


26

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:

  1. 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 .

  2. 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.)

  3. Informieren Sie Ihr Entwicklungsteam über die Probleme mit Multithread-Code.

  4. Was würdest du tun?


23
Einige Leute, die mit einem Problem konfrontiert sind, denken, dass ich Multi-Threading verwenden werde. Jetzt haben sie zwei Sonden
Tom Squires

20
Was für mich gut funktioniert, ist, wenn immer möglich, die Veränderbarkeit loszuwerden . Wenn sich der Status eines veränderlichen Objekts ändert, um den neuen Wert zu übergeben, versuche ich, diesen in die Übergabe eines neuen unveränderlichen Objekts mit geändertem Wert umzuwandeln. Wenn die Objektinitialisierung sicher durchgeführt wird, ist die Abwesenheit von
Datenrennen

3
Willkommen in der Multi-Thread-Hölle. Ich schreibe seit über 20 Jahren Multithread- / Paralell-Programme in Ada, Occam, C ++. Es ist nie einfach, alles erfordert sehr sorgfältige Überlegungen, und jeder, der sagt, "es ist einfach, mach X", ist ein Narr, der nicht wirklich versteht, was los ist. Viel Glück.
quick_now

2
Wenn Sie möchten, dass die Parallelität gut funktioniert, verwenden Sie Erlang! In Wahrheit wollen Sie eine Art gemeinsames Nichts-Schauspieler-Modell, in dem die seltsamen Eckfälle beseitigt werden.
Zachary K

3
@DeadMG Ich würde behaupten, dass die Parallelität von gemeinsam genutzten Zuständen von Natur aus anfällig für seltsame Eckfälle ist und vermieden werden sollte. Aber Hey, ich habe ein Buch über Erlang
Zachary K

Antworten:


27

Abgesehen davon weist Ihr Code wichtige andere Probleme auf. Zeiger manuell löschen? Aufrufen einer cleanupFunktion? Owch. Wie im Fragekommentar genau ausgeführt, verwenden Sie RAII nicht für Ihre Sperre, was ebenfalls ein ziemlich epischer Fehler ist und garantiert, dass DoSomethingImportantschreckliche Dinge passieren , wenn eine Ausnahme ausgelöst wird.

Die Tatsache, dass dieser Multithread-Fehler auftritt, ist nur ein Symptom des Kernproblems: Ihr Code hat in jeder Threading-Situation eine äußerst schlechte Semantik und Sie verwenden völlig unzuverlässige Tools und Ex-Idiome. Wenn ich Sie wäre, wäre ich erstaunt, dass es mit einem einzigen Thread funktioniert, geschweige denn mit mehr.

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.

Der springende Punkt bei der Referenzzählung ist, dass der Thread seine Instanz bereits freigegeben hat . Wenn nicht, kann es nicht zerstört werden, da der Thread noch einen Verweis hat.

Verwenden Sie std::shared_ptr. Wenn alle Threads freigegeben wurden (und daher niemand die Funktion aufrufen kann, da er keinen Zeiger darauf hat), wird der Destruktor aufgerufen. Dies ist garantiert sicher.

Verwenden Sie zweitens eine echte Threading-Bibliothek, z. B. die Thread-Bausteine ​​von Intel oder die Parallel Patterns Library von Microsoft. Das Schreiben Ihres eigenen Codes ist zeitaufwändig und unzuverlässig, und der Code steckt voller Threading-Details, die er nicht benötigt. Das Erstellen eigener Sperren ist genauso schlimm wie das Erstellen einer eigenen Speicherverwaltung. Sie haben bereits viele allgemeine, sehr nützliche Threading-Redewendungen implementiert, die für Ihre Verwendung korrekt funktionieren.


Dies ist eine gute Antwort, aber nicht die Richtung, nach der ich gesucht habe, da es zu viel Zeit kostet, einen Teil des Beispielcodes zu bewerten, der nur der Einfachheit halber geschrieben wurde (und unseren tatsächlichen Code in unserem Produkt nicht widerspiegelt). Aber ich bin gespannt auf einen Kommentar, den Sie gemacht haben - "unzuverlässige Werkzeuge". Was ist ein unzuverlässiges Werkzeug? Welche Tools empfehlen Sie?
koncurrency

5
@koncurrency: Ein unzuverlässiges Tool ist eine Sache wie manuelle Speicherverwaltung oder das Schreiben einer eigenen Synchronisation, bei der theoretisch ein Problem X gelöst wird, in Wirklichkeit jedoch so schlimm ist, dass Sie so gut wie riesige Fehler garantieren können und nur so das Problem möglicherweise gelöst werden kann In angemessenem Umfang ist dies durch den massiven und unverhältnismäßigen Zeitaufwand für Entwickler möglich - genau das, was Sie gerade haben.
DeadMG

9

Andere Poster haben gut kommentiert, was getan werden sollte, um die Kernprobleme zu beheben. Dieser Beitrag befasst sich mit dem unmittelbareren Problem, den alten Code so gut zu patchen, dass Sie Zeit haben, alles richtig zu wiederholen. Mit anderen Worten, dies ist nicht der richtige Weg , Dinge zu tun, es ist nur ein Weg, vorerst zu humpeln.

Ihre Idee, wichtige Ereignisse zu konsolidieren, ist ein guter Anfang. Ich würde so weit gehen, einen einzelnen Dispatch-Thread zu verwenden, um alle Schlüsselsynchronisationsereignisse zu behandeln, wo immer es eine Auftragsabhängigkeit gibt. Richten Sie eine thread-sichere Nachrichtenwarteschlange ein und veranlassen Sie die Ausführung oder den Auslöser des Vorgangs, wo immer Sie gegenwärtig parallele Vorgänge ausführen (Zuordnungen, Bereinigungen, Rückrufe usw.). Senden Sie stattdessen eine Nachricht an diesen Thread. Die Idee ist, dass dieser eine Thread alle Starts, Stopps, Zuordnungen und Aufräumarbeiten der Arbeitseinheit steuert.

Der Dispatch-Thread löst die von Ihnen beschriebenen Probleme nicht , sondern konsolidiert sie nur an einer Stelle. Sie müssen sich immer noch um Ereignisse / Nachrichten sorgen, die in unerwarteter Reihenfolge auftreten. Ereignisse mit erheblichen Laufzeiten müssen weiterhin an andere Threads gesendet werden, sodass weiterhin Probleme mit der gemeinsamen Nutzung freigegebener Daten auftreten. Eine Möglichkeit, dies zu verringern, besteht darin, die Weitergabe von Daten als Referenz zu vermeiden. Wann immer möglich, sollten die Daten in Versandnachrichten Kopien sein, die dem Empfänger gehören. (Dies entspricht der von anderen erwähnten Unveränderlichkeit von Daten.)

Der Vorteil dieses Dispatch-Ansatzes besteht darin, dass Sie innerhalb des Dispatch-Threads eine Art sicheren Hafen haben, in dem Sie zumindest wissen, dass bestimmte Vorgänge nacheinander ausgeführt werden. Der Nachteil ist, dass es zu einem Engpass und zusätzlichem CPU-Overhead kommt. Ich schlage vor, sich zunächst nicht um eines dieser Dinge zu kümmern: Konzentrieren Sie sich zunächst darauf, ein gewisses Maß an korrekter Funktionsweise zu erreichen, indem Sie so viel wie möglich in den Versand-Thread verschieben. Führen Sie dann eine Profilerstellung durch, um festzustellen, was die meiste CPU-Zeit in Anspruch nimmt, und verschieben Sie es mithilfe der richtigen Multithreading-Techniken wieder aus dem Dispatch-Thread.

Wiederum beschreibe ich nicht die richtige Vorgehensweise, sondern einen Prozess, der Sie in Schritten auf den richtigen Weg bringen kann, die klein genug sind, um die kommerziellen Fristen einzuhalten.


+1 für einen vernünftigen Zwischenvorschlag, um die bestehende Herausforderung zu meistern.

Ja, das ist der Ansatz, den ich untersuche. Sie sprechen gute Punkte zur Leistung an.
koncurrency

Das Ändern von Dingen, die über einen einzelnen Versand-Thread laufen, klingt für mich nicht nach einem schnellen Patch, sondern nach einem massiven Umgestalter.
Sebastian Redl

8

Basierend auf dem angezeigten Code haben Sie einen Stapel WTF. Es ist äußerst schwierig, wenn nicht unmöglich, eine schlecht geschriebene Multithread-Anwendung schrittweise zu reparieren. Teilen Sie den Eigentümern mit, dass die Anwendung ohne erhebliche Nacharbeit niemals zuverlässig ist. Geben Sie ihnen eine Schätzung basierend auf der Überprüfung und Überarbeitung jedes einzelnen Teils des Codes, der mit gemeinsam genutzten Objekten interagiert. Geben Sie ihnen zuerst einen Kostenvoranschlag für die Inspektion. Dann können Sie einen Kostenvoranschlag für die Nacharbeit abgeben.

Wenn Sie den Code überarbeiten, sollten Sie planen, den Code so zu schreiben, dass er nachweislich korrekt ist. Wenn Sie nicht wissen, wie man das macht, finden Sie jemanden, der es tut, oder Sie werden am selben Ort enden.


Lies das jetzt, nachdem meine Antwort positiv bewertet wurde. Ich wollte nur sagen, dass ich den einleitenden Satz liebe :)
back2dos

7

Wenn Sie etwas Zeit für die Umgestaltung Ihrer Anwendung haben, empfehle ich Ihnen, sich das Akteurmodell anzusehen (siehe z. B. Theron , Casablanca , libcppa , CAF für C ++ - Implementierungen).

Akteure sind Objekte, die gleichzeitig ausgeführt werden und nur über den asynchronen Nachrichtenaustausch miteinander kommunizieren. Alle Probleme des Thread-Managements, der Mutexe, Deadlocks usw. werden von einer Actor-Implementierungsbibliothek behandelt, und Sie können sich darauf konzentrieren, das Verhalten Ihrer Objekte (Actors) zu implementieren, was darauf hinausläuft, die Schleife zu wiederholen

  1. Erhalte Nachricht
  2. Berechnung durchführen
  3. Nachricht (en) senden / andere Akteure erstellen / töten.

Ein Ansatz für Sie könnte darin bestehen, zuerst etwas über das Thema zu lesen und sich möglicherweise eine oder zwei Bibliotheken anzusehen, um zu sehen, ob das Akteurmodell in Ihren Code integriert werden kann.

Ich benutze dieses Modell (eine vereinfachte Version) seit einigen Monaten in einem meiner Projekte und bin erstaunt, wie robust es ist.


1
Die Akka-Bibliothek für Scala ist eine nette Implementierung davon, die viel darüber nachdenkt, wie man Elternschauspieler tötet, wenn Kinder sterben oder umgekehrt. Ich weiß, es ist nicht C ++, aber einen Blick wert: akka.io
GlenPeterson

1
@GlenPeterson: Danke, ich kenne akka (die ich momentan als die interessanteste Lösung betrachte und die sowohl mit Java als auch mit Scala funktioniert), aber die Frage befasst sich speziell mit C ++. Ansonsten könnte man event über Erlang nachdenken. Ich denke, in Erlang sind alle Kopfschmerzen der Multithreading-Programmierung endgültig verschwunden. Aber vielleicht kommen Frameworks wie Akka sehr nahe.
Giorgio

"Ich denke, in Erlang sind alle Kopfschmerzen der Multithreading-Programmierung endgültig verschwunden." Ich denke, das ist vielleicht etwas übertrieben. Wenn dies zutrifft, fehlt möglicherweise die Leistung. Ich weiß, dass Akka nicht mit C ++ funktioniert, nur, dass es für die Verwaltung mehrerer Threads nach dem neuesten Stand der Technik aussieht. Es ist jedoch nicht threadsicher. Sie können immer noch einen veränderlichen Zustand zwischen Schauspielern passieren und sich in den Fuß schießen.
GlenPeterson

Ich bin kein Erlang-Experte, aber jeder AFAIK-Akteur wird isoliert hingerichtet und unveränderliche Botschaften werden ausgetauscht. Sie müssen sich also wirklich überhaupt nicht mit Threads und dem gemeinsam genutzten veränderlichen Status befassen. Die Leistung ist wahrscheinlich geringer als in C ++. Dies geschieht jedoch immer dann, wenn Sie die Abstraktionsebene erhöhen (Sie erhöhen die Ausführungszeit, reduzieren aber die Entwicklungszeit).
Giorgio

Kann der Downvoter bitte einen Kommentar hinterlassen und vorschlagen, wie ich diese Antwort verbessern kann?
Giorgio

6

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:

Der Fehler ist hier nicht das "Vergessen", sondern das "Nicht-Reparieren". Wenn Dinge in unerwarteter Reihenfolge passieren, haben Sie ein Problem. Sie sollten es lösen, anstatt zu versuchen, es zu umgehen (ein Schloss auf etwas zu klopfen, ist normalerweise ein Workaround).

Sie sollten versuchen, das Darstellermodell / Messaging bis zu einem gewissen Grad anzupassen und eine getrennte Betroffenheit zu haben. Die Aufgabe von Fooist eindeutig, irgendeine Art von HTTP-Kommunikation zu handhaben. Wenn Sie Ihr System so gestalten möchten, dass dies parallel erfolgt, muss die darüber liegende Ebene den Objektlebenszyklus behandeln und entsprechend auf die Synchronisierung zugreifen.

Der Versuch, mehrere Threads mit denselben veränderlichen Daten zu betreiben, ist schwierig. Es ist aber auch selten notwendig. Alle gängigen Fälle, in denen dies erforderlich ist, wurden bereits in übersichtlicheren Konzepten zusammengefasst und mehrmals für etwa alle wichtigen Imperativsprachen implementiert. Sie müssen sie nur benutzen.


2

Ihre Probleme sind ziemlich schlimm, aber typisch für die schlechte Nutzung von C ++. Die Codeüberprüfung behebt einige dieser Probleme. 30 Minuten, ein Augapfel-Set ergibt 90% der Ergebnisse. (Zitat dafür ist googleable)

# 1 Problem Sie müssen sicherstellen, dass es eine strikte Sperrhierarchie gibt, um zu verhindern, dass das Sperren blockiert.

Wenn Sie Autolock durch einen Wrapper und ein Makro ersetzen, können Sie dies tun.

Behalten Sie eine statische globale Karte der auf der Rückseite Ihres Wrappers erstellten Sperren bei. Sie verwenden ein Makro, um die Informationen zu Finename und Zeilennummer in den Autolock-Wrapper-Konstruktor einzufügen.

Sie benötigen außerdem ein statisches Dominator-Diagramm.

Jetzt müssen Sie innerhalb der Sperre das Dominator-Diagramm aktualisieren. Wenn Sie eine Bestelländerung erhalten, machen Sie einen Fehler geltend und brechen ab.

Nach ausgiebigen Tests sind Sie möglicherweise von den meisten latenten Deadlocks befreit.

Der Code wird als Übung für den Schüler hinterlassen.

Problem Nr. 2 wird dann (meistens) verschwinden

Ihre archientualische Lösung wird funktionieren. Ich habe es schon in missions- und lebenskritischen Systemen verwendet. Ich nehme es so an

  • Übergeben Sie unveränderliche Objekte oder machen Sie Kopien davon, bevor Sie sie übergeben.
  • Teilen Sie keine Daten über öffentliche Variablen oder Getter.

  • Externe Ereignisse gehen über einen Multithread-Versand in eine Warteschlange ein, die von einem Thread bedient wird. Jetzt können Sie eine Art Grund für die Ereignisbehandlung angeben.

  • Datenänderungen, bei denen Cross-Threads in eine thread-sichere Warteschlange geraten, werden von einem Thread verarbeitet. Abonnements machen. Jetzt können Sie eine Art Grund für Datenflüsse angeben.

  • Wenn Ihre Daten stadtübergreifend sein müssen, veröffentlichen Sie sie in der Datenwarteschlange. Dadurch wird es kopiert und asynchron an die Abonnenten übergeben. Bricht auch alle Datenabhängigkeiten im Programm.

Dies ist so ziemlich ein billiges Schauspielermodell. Giorgios Links werden helfen.

Schließlich Ihr Problem mit heruntergefahrenen Objekten.

Bei der Referenzzählung haben Sie 50% gelöst. Die anderen 50% beziehen sich auf die Anzahl der Rückrufe. Pass-Rückruf-Inhaber erhalten eine Referenz. Der Abschaltaufruf muss dann auf die Nullzählung auf der Nachzählung warten. Löst keine komplizierten Objektgraphen; das ist immer in echte Müllabfuhr. (Was ist die Motivation in Java, keine Versprechungen darüber zu machen, wann oder ob finalize () aufgerufen wird; um Sie davon abzuhalten, auf diese Weise zu programmieren.)


2

Für zukünftige Entdecker: Um die Antwort zum Akteurmodell zu ergänzen, möchte ich CSP ( Communicating Sequential Processes ) hinzufügen , mit einer Anspielung auf die größere Familie von Prozesskalkülen, in denen CSP enthalten ist. CSP ähnelt dem Akteurmodell, ist jedoch unterschiedlich aufgeteilt. Sie haben immer noch eine Reihe von Threads, die jedoch nicht spezifisch miteinander, sondern über bestimmte Kanäle kommunizieren, und beide Prozesse müssen zum Senden bzw. Empfangen bereit sein, bevor dies geschieht. Es gibt auch eine formalisierte Sprache für den Nachweis des korrekten CSP-Codes. Ich arbeite immer noch intensiv mit CSP, aber ich verwende es jetzt seit einigen Monaten in einigen Projekten, und es ist stark vereinfacht.

Die University of Kent hat eine C ++ - Implementierung ( https://www.cs.kent.ac.uk/projects/ofa/c++csp/ , geklont unter https://github.com/themasterchef/cppcsp2 ).


1

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.

Ich lese gerade das und es erklärt alle Probleme, die Sie bekommen können und wie Sie sie vermeiden können, in C ++ (unter Verwendung der neuen Threading-Bibliothek, aber ich denke, die globalen Erklärungen sind für Ihren Fall gültig): http: //www.amazon. com / C-Concurrency-Action-Practical-Multithreading / dp / 1933988770 / ref = sr_1_1? ie = UTF8 & qid = 1337934534 & sr = 8-1

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.)

Ich persönlich verwende eine vereinfachte UML und gehe einfach davon aus, dass Nachrichten asynchron verarbeitet werden. Dies gilt auch zwischen "Modulen", aber innerhalb von Modulen möchte ich nicht wissen müssen.

Informieren Sie Ihr Entwicklungsteam über die Probleme mit Multithread-Code.

Das Buch würde helfen, aber ich denke, Übungen / Prototyping und erfahrener Mentor wären besser.

Was würdest du tun?

Ich würde völlig vermeiden, dass Leute, die Parallelitätsprobleme nicht verstehen, an dem Projekt arbeiten. Aber ich denke, Sie können das nicht tun. In Ihrem speziellen Fall habe ich keine Ahnung, außer dass Sie versuchen, das Team besser auszubilden.


Danke für den Buchvorschlag. Ich werde es wahrscheinlich abholen.
koncurrency

Das Einfädeln ist sehr schwierig. Nicht jeder Programmierer ist der Herausforderung gewachsen. In der Geschäftswelt waren die verwendeten Threads jedes Mal von Sperren umgeben, sodass keine zwei Threads gleichzeitig ausgeführt werden konnten. Es gibt Regeln, die Sie befolgen können, um es einfacher zu machen, aber es ist immer noch schwierig.
GlenPeterson

@ GlenPeterson Einverstanden, jetzt, da ich mehr Erfahrung habe (seit dieser Antwort), finde ich, dass wir bessere Abstraktionen brauchen, um sie handhabbar zu machen und das Teilen von Daten zu verhindern. Glücklicherweise scheinen Sprachdesigner hart daran zu arbeiten.
Klaim 12.10.12

Ich war wirklich beeindruckt von Scala, insbesondere, weil es die Vorteile der Unveränderlichkeit und der minimalen Nebenwirkungen der funktionalen Programmierung für Java bringt, das direkt von C ++ abstammt. Es wird auf der Java Virtual Machine ausgeführt und bietet möglicherweise nicht die Leistung, die Sie benötigen. In Joshua Blochs Buch "Effective Java" geht es darum, die Veränderlichkeit zu minimieren, luftdichte Schnittstellen zu schaffen und die Thread-Sicherheit zu gewährleisten. Ich wette, Sie können 80-90% davon auf C ++ anwenden, obwohl es auf Java basiert. Das Hinterfragen der Veränderlichkeit und des gemeinsam genutzten Zustands (oder der Veränderlichkeit des gemeinsam genutzten Zustands) in Ihren Codeüberprüfungen kann ein guter erster Schritt für Sie sein.
GlenPeterson

1

Sie sind bereits unterwegs, indem Sie das Problem erkennen und aktiv nach einer Lösung suchen. Folgendes würde ich tun:

  • Setzen Sie sich und entwerfen Sie ein Threading-Modell für Ihre Anwendung. Dieses Dokument beantwortet Fragen wie: Welche Arten von Threads haben Sie? Was soll in welchem ​​Thread gemacht werden? Welche verschiedenen Arten von Synchronisationsmustern sollten Sie verwenden? Mit anderen Worten, es sollte die "Einsatzregeln" bei der Bekämpfung von Multithreading-Problemen beschreiben.
  • Verwenden Sie Thread-Analyse-Tools, um Ihre Codebasis auf Fehler zu überprüfen. Valgrind hat einen Thread-Checker namens Helgrind, mit dem man Dinge wie den gemeinsam genutzten Zustand erkennen kann, der ohne ordnungsgemäße Synchronisation manipuliert wird. Es gibt mit Sicherheit noch andere gute Werkzeuge, suchen Sie sie.
  • Ziehen Sie eine Migration von C ++ in Betracht. C ++ ist ein Alptraum, in dem man parallele Programme schreiben kann. Meine persönliche Wahl wäre Erlang , aber das ist Geschmackssache.

8
Definitiv -1 für das letzte Bit. Es scheint, dass der OP-Code die primitivsten Werkzeuge und nicht die tatsächlichen C ++ - Werkzeuge verwendet.
DeadMG

2
Ich stimme nicht zu. Parallelität in C ++ ist ein Albtraum, selbst wenn Sie die richtigen C ++ - Mechanismen und -Tools verwenden. Und bitte beachte, dass ich die Formulierung " Überlegen " gewählt habe. Ich verstehe vollkommen, dass es vielleicht keine realistische Alternative ist, aber bei C ++ zu bleiben, ohne die Alternativen in Betracht zu ziehen, ist einfach albern.
JesperE

4
@JesperE - Entschuldigung, aber nein. Parallelität in C ++ ist nur dann ein Albtraum, wenn Sie es zu einem machen, indem Sie zu niedrig gehen. Verwenden Sie eine ordnungsgemäße Thread-Abstraktion und sie ist nicht schlechter als jede andere Sprache oder Laufzeit. Und mit der richtigen Anwendungsstruktur ist es tatsächlich so einfach wie alles, was ich je gesehen habe.
Michael Kohne

2
Wo ich arbeite, haben wir meiner Meinung nach eine ordnungsgemäße Anwendungsstruktur, verwenden die richtigen Threading-Abstraktionen und so weiter. Trotzdem haben wir im Laufe der Jahre unzählige Stunden damit verbracht, Fehler zu beheben, die in Sprachen, die für die gleichzeitige Verwendung ausgelegt sind, einfach nicht angezeigt würden. Aber ich habe das Gefühl, dass wir uns einigen müssen, wenn wir uns nicht einig sind.
JesperE

1
@JesperE: Ich stimme dir zu. Das Erlang-Modell (für das es Implementierungen für Scala / Java, Ruby und meines Wissens auch für C ++ gibt) ist wesentlich robuster als das direkte Codieren mit Threads.
Giorgio

1

Betrachten Sie Ihr Beispiel: Sobald Foo :: Shutdown ausgeführt wird, darf es nicht mehr möglich sein, OnHttpRequestComplete aufzurufen, um ausgeführt zu werden. Das hat nichts mit einer Implementierung zu tun, es kann einfach nicht funktionieren.

Sie könnten auch argumentieren, dass Foo :: Shutdown nicht aufrufbar sein sollte, während ein Aufruf von OnHttpRequestComplete ausgeführt wird (definitiv wahr) und wahrscheinlich nicht, wenn ein Aufruf von OnHttpRequestComplete noch aussteht.

Das Erste, was richtig ist, ist nicht das Sperren usw., sondern die Logik dessen, was erlaubt ist oder nicht. Ein einfaches Modell wäre, dass Ihre Klasse keine oder mehr unvollständige Anforderungen, keine oder mehr noch nicht aufgerufene Abschlüsse, keine oder mehr laufende Abschlüsse hat und dass Ihr Objekt heruntergefahren werden soll oder nicht.

Es wird erwartet, dass Foo :: Shutdown die Ausführung von Abschlüssen abschließt, unvollständige Anforderungen so weit ausführt, dass sie nach Möglichkeit heruntergefahren werden können, dass keine weiteren Abschlüsse mehr gestartet werden können und dass keine weiteren Anforderungen gestartet werden können.

Was Sie tun müssen: Fügen Sie Ihren Funktionen Spezifikationen hinzu, die genau angeben, was sie tun werden. (Das Starten einer http-Anforderung kann beispielsweise fehlschlagen, nachdem Shutdown aufgerufen wurde.) Und dann schreiben Sie Ihre Funktionen so, dass sie den Spezifikationen entsprechen.

Sperren werden am besten nur für den kleinstmöglichen Zeitraum verwendet, um die Änderung gemeinsam genutzter Variablen zu steuern. Sie haben also möglicherweise eine Variable "performingShutDown", die durch eine Sperre geschützt ist.


0

Was würdest du tun?

Um ehrlich zu sein; Ich würde schnell weglaufen.

Nebenläufigkeitsprobleme sind SCHLECHT . Etwas kann monatelang perfekt funktionieren und dann (aufgrund des spezifischen Timings mehrerer Dinge) plötzlich im Gesicht des Kunden aufblähen, ohne herauszufinden, was passiert ist, ohne die Hoffnung, jemals einen schönen (reproduzierbaren) Fehlerbericht zu sehen und ohne die Möglichkeit um sicherzugehen, dass es sich nicht um einen Hardwarefehler handelte, der nichts mit der Software zu tun hatte.

Das Vermeiden von Parallelitätsproblemen muss während der Entwurfsphase beginnen und genau mit der Vorgehensweise beginnen ("globale Sperrreihenfolge", Akteurmodell, ...). Es ist nicht etwas, das Sie versuchen, in einer wahnsinnigen Panik zu beheben, in der Hoffnung, dass sich nach einer bevorstehenden Veröffentlichung nicht alles selbst zerstört.

Beachten Sie, dass ich hier nicht scherze. Ihre eigenen Worte ("Das meiste 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. ") Weisen darauf hin, dass all diese Erfahrungen, die die Leute bereits gemacht haben, was ich getan habe schlage vor.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.