Ich bin in den letzten Jahren mehrmals mit diesem Problem konfrontiert worden, als ich für mehrere Projekte Code zur Behandlung von Threads geschrieben habe. Ich antworte mit Verspätung, weil die meisten anderen Antworten zwar Alternativen bieten, aber die Frage nach dem Testen nicht wirklich beantworten. Meine Antwort richtet sich an die Fälle, in denen es keine Alternative zu Multithreading-Code gibt; ich gehe der Vollständigkeit halber auch auf Fragen des Code-Designs ein, bespreche aber auch Unit-Tests.
Schreiben von testbarem Multithreading-Code
Als Erstes müssen Sie den Code für die Behandlung der Produktions-Threads von dem Code trennen, der die eigentliche Datenverarbeitung durchführt. Auf diese Weise kann die Datenverarbeitung als Single-Thread-Code getestet werden, und der Multi-Thread-Code dient lediglich der Koordinierung der Threads.
Die zweite Sache, die man bedenken sollte, ist, dass Fehler in Multithreading-Code probabilistisch sind; die Fehler, die sich am wenigsten häufig manifestieren, sind die Fehler, die sich in die Produktion einschleichen werden, die selbst in der Produktion schwer zu reproduzieren sind und daher die größten Probleme verursachen werden. Aus diesem Grund ist die Standardmethode, den Code schnell zu schreiben und ihn dann zu debuggen, bis er funktioniert, eine schlechte Idee für Multithreading-Code; sie wird zu Code führen, in dem die einfachen Fehler behoben sind und die gefährlichen Fehler noch vorhanden sind.
Stattdessen müssen Sie beim Schreiben von Multithreading-Code den Code mit der Einstellung schreiben, dass Sie die Fehler von vornherein vermeiden wollen. Wenn Sie den Datenverarbeitungscode ordnungsgemäß entfernt haben, sollte der Code für die Thread-Verarbeitung klein genug sein - vorzugsweise ein paar Zeilen, schlimmstenfalls ein paar Dutzend Zeilen -, dass Sie eine Chance haben, ihn zu schreiben, ohne einen Fehler zu schreiben, und sicherlich ohne viele Fehler zu schreiben, wenn Sie Threading verstehen, sich Zeit nehmen und vorsichtig sind.
Schreiben von Unit-Tests für multithreadingfähigen Code
Sobald der Multithreading-Code so sorgfältig wie möglich geschrieben ist, lohnt es sich, Tests für diesen Code zu schreiben. Der Hauptzweck der Tests besteht nicht so sehr darin, auf stark zeitabhängige Race-Condition-Fehler zu testen - es ist unmöglich, auf solche Race-Conditions wiederholbar zu testen -, sondern vielmehr darin, zu prüfen, ob Ihre Sperrstrategie zur Vermeidung solcher Fehler die beabsichtigte Interaktion mehrerer Threads ermöglicht.
Um das korrekte Sperrverhalten zu testen, muss ein Test mehrere Threads starten. Um den Test wiederholbar zu machen, sollen die Interaktionen zwischen den Threads in einer vorhersehbaren Reihenfolge ablaufen. Wir wollen die Threads im Test nicht extern synchronisieren, da dies Fehler verdeckt, die in der Produktion auftreten könnten, wenn die Threads nicht extern synchronisiert sind. Damit bleibt nur die Verwendung von Zeitverzögerungen für die Thread-Synchronisierung, die ich immer dann erfolgreich eingesetzt habe, wenn ich Tests für Multithreading-Code schreiben musste.
Wenn die Verzögerungen zu kurz sind, wird der Test anfällig, weil geringfügige Zeitunterschiede - z. B. zwischen verschiedenen Rechnern, auf denen die Tests ausgeführt werden - dazu führen können, dass das Timing nicht stimmt und der Test fehlschlägt. In der Regel beginne ich mit Verzögerungen, die zu Testfehlern führen, erhöhe die Verzögerungen, so dass der Test auf meinem Entwicklungsrechner zuverlässig läuft, und verdopple dann die Verzögerungen, so dass der Test eine gute Chance hat, auf anderen Rechnern zu laufen. Das bedeutet, dass der Test eine makroskopische Zeitspanne in Anspruch nimmt, aber meiner Erfahrung nach kann man diese Zeitspanne mit einem sorgfältigen Testdesign auf nicht mehr als ein Dutzend Sekunden begrenzen. Da es in Ihrer Anwendung nicht sehr viele Stellen gibt, an denen Thread-Koordinationscode erforderlich ist, sollte dies für Ihre Testsuite akzeptabel sein.
Verfolgen Sie schließlich die Anzahl der Fehler, die durch Ihren Test gefunden wurden. Wenn Ihr Test eine Codeabdeckung von 80 % hat, kann man davon ausgehen, dass er etwa 80 % der Fehler abfängt. Wenn Ihr Test gut konzipiert ist, aber keine Fehler findet, ist die Wahrscheinlichkeit groß, dass Sie keine zusätzlichen Fehler haben, die erst in der Produktion auftauchen werden. Wenn der Test einen oder zwei Fehler findet, haben Sie vielleicht noch Glück. Wenn das nicht der Fall ist, sollten Sie eine sorgfältige Überprüfung oder sogar eine komplette Neuschreibung Ihres Thread-Handling-Codes in Erwägung ziehen, da es wahrscheinlich ist, dass der Code noch versteckte Fehler enthält, die sehr schwer zu finden sind, bis der Code in Produktion ist, und dann sehr schwer zu beheben sind.
2 Stimmen
Ich hatte vor, eine Frage zu genau diesem Thema zu stellen. Obwohl Will im Folgenden viele gute Punkte anführt, denke ich, dass wir es besser machen können. Ich stimme zu, dass es keinen einzigen "Ansatz" gibt, um das Problem sauber zu lösen. Aber "so gut wie möglich zu testen" setzt die Messlatte sehr niedrig an. Ich werde mit meinen Ergebnissen zurückkommen.
0 Stimmen
In Java: Das Paket java.util.concurrent enthält einige schlecht bekannte Klassen, die helfen können, deterministische JUnit-Tests zu schreiben. Werfen Sie einen Blick auf - CountDownLatch - Semaphor - Austauscher
0 Stimmen
Können Sie bitte einen Link zu Ihrer vorherigen Frage zu Unit-Tests angeben?
0 Stimmen
@Andrew Grimm: stackoverflow.com/questions/11060/
0 Stimmen
stackoverflow.com/questions/4418373/
20 Stimmen
Ich denke, es ist wichtig, darauf hinzuweisen, dass diese Frage 8 Jahre alt ist und die Anwendungsbibliotheken in der Zwischenzeit einen weiten Weg zurückgelegt haben. In der "modernen Ära" (2016) kommt Multi-Thread-Entwicklung vor allem in eingebetteten Systemen vor. Wenn Sie jedoch an einer Desktop- oder Telefonanwendung arbeiten, sollten Sie sich zunächst über die Alternativen informieren. Anwendungsumgebungen wie .NET enthalten inzwischen Tools, die wahrscheinlich 90 % der üblichen Multi-Threading-Szenarien verwalten oder stark vereinfachen. (asnync/await, PLinq, IObservable, die TPL...). Multithreading-Code ist schwierig. Wenn man das Rad nicht neu erfindet, muss man es auch nicht neu testen.
0 Stimmen
Dies mag eine unpopuläre Idee sein, aber wenn Sie Ihren Code in Rust schreiben, ohne
unsafe
Blöcken kann der Compiler tatsächlich rennfreie Threadsicherheit garantieren. Für mich ist dies ein noch wichtigeres Merkmal von Rust als die Speichersicherheit, die normalerweise erwähnt wird.