Latente Defekte zutage fördern

Multi-Core-Verarbeitung deckt auf

Latente Defekte
zutage fördern

Bestehende softwarebasierte Systeme bedürfen gelegentlich der Modernisierung. Aus Gründen der Kosteneffektivität bietet es sich bei dieser Gelegenheit häufig an, eine Migration auf einen Multi-Core-Prozessor vorzunehmen, denn verglichen mit Prozessoren, die nur über einen Kern verfügen, bergen Multi-Core-Prozessoren das Potenzial einer deutlichen Performance-Steigerung.
Um das gebotene Performance-Potenzial jedoch wirklich ausschöpfen zu können, muss der Code für den nebenläufigen Betrieb geschrieben sein. Den bestehenden Code entsprechend umzuschreiben, ist jedoch eine durchaus riskante Sache, denn bekanntermaßen besteht bei jeder Änderung an einer Software die Gefahr, dass sich neue Defekte einschleichen. Hinzu kommen die mit der Nebenläufigkeit zusammenhängenden Defekte wie etwa Data Races, Deadlocks und Starvation. Diese kommen nur allzu leicht vor, sind dafür aber extrem schwierig zu finden. Wenn der ursprüngliche Code auf einer Single-Thread-Struktur basierte, versteht es sich von selbst, dass bei der Migration größte Vorsicht angewendet werden muss. Weniger offensichtlich ist dagegen die Tatsache, dass auch ein Code, der bereits in mehrere Threads gegliedert ist und vielleicht schon jahrelang ohne Probleme auf einem Single-Core-Prozessor gelaufen ist, ein hohes Fehlerrisiko birgt, wenn er auf einmal von einem Multi-Core-Prozessor verarbeitet wird. Um die Gründe hierfür zu erläutern, muss auf die subtilsten Bugs eingegangen werden, die in nebenläufigen Systemen auftreten können. Gemeint sind die so genannten Data Races.

Data Races

Es handelt sich dabei um Race Conditions, die bei Zugriffen auf einen gemeinsam genutzten Speicher auftreten können. Zu einem Data Race kommt es, wenn mehrere Verarbeitungs-Threads auf eine gemeinsam genutzte Speicherstelle zugreifen, wenn mindestens einer dieser Threads an diese Speicherstelle schreibt und wenn die Zugriffe nicht durch eine explizite Synchronisation (z.B. durch Freigabe eines Mutex oder Setzen eines Semaphoren) separiert werden. Das Einkreisen und Diagnostizieren von Data Races gestaltet sich recht schwierig, denn die Speicherinhalte werden durch die Data Races nicht unbedingt verfälscht. Kommt es zu einer Verfälschung des Speicherinhalts, muss dies außerdem nicht in jedem Fall einen von außen erkennbaren Bug auslösen. Ob Data Races einen Ausfall zur Folge haben, hängt in hohem Maße vom Timing des jeweiligen Verarbeitungsdurchgangs ab. Ein bestimmter Testfall kann hunderte Male bei exakt gleichen Eingangsbedingungen erfolgreich ablaufen, um dann aus unerfindlichen Gründen fehlzuschlagen, weil eine Race Condition eine unerwünschte Reaktion ausgelöst hat. Tritt ein Funktionsfehler auf, ist er schwierig zu diagnostizieren, da die genaue Abfolge der Speicherzugriffe, die die Störung hervorgerufen hat, nur schwierig zu reproduzieren ist.

Verzahnung von Speicherzugriffen

Was Data Races so problematisch macht, ist die Tatsache, dass die Art und Weise, auf die die Speicherzugriffe der verschiedenen Threads ineinandergreifen, das heißt miteinander verzahnt sind, weitgehend nicht-deterministisch erfolgt. Selbst bei Programmen von recht überschaubarem Umfang ist die Zahl möglicher Verzahnungen enorm hoch, sodass es unmöglich ist, alle denkbaren Kombinationen zu überprüfen.

Single-Core

Programme, die sich in mehrere Threads gliedern, verhalten sich auf Single-Core-Prozessoren tendenziell unproblematischer, weil die Nebenläufigkeit hier nur virtueller Natur ist. Die Threads arbeiten hier in Wirklichkeit nicht parallel zueinander, sondern nutzen abwechselnd den einzigen vorhandenen Core. Die Threading-Bibliothek oder das Betriebssystem kümmern sich um das Scheduling der Threads mit dem Ziel, eine parallele Verarbeitung zu simulieren. Da Kontextwechsel stets mehrere Befehle erfordern, ist es ineffizient, zu viele Kontextwechsel vorzunehmen. Der Scheduler ist aus diesem Grund bestrebt, ihre Anzahl zu minimieren. Die CPU führt deshalb möglicherweise einige Tausend Instruktionen von Thread A aus, um anschließend ein paar Millionen Befehle von Thread B zu verarbeiten und anschließend wieder zu Thread A zu wechseln usw. Dies führt dazu, dass die Abfolge der Speicherzugriffs-Ereignisse der einzelnen Threads nur grob miteinander verzahnt wird.

Multi-Core

Auf einem Multi-Core-Prozessor kommt es dagegen zu einer echten, physischen Nebenläufigkeit. Die verschiedenen Threads arbeiten tatsächlich parallel zueinander und die Speicherzugriffs-Ereignisse werden wesentlich feiner miteinander verzahnt, wie es Bild 1 verdeutlicht. Bei der eher groben Verzahnung, zu der es bei der Verarbeitung durch einen Single-Core-Prozessor kommt, ist es wesentlich unwahrscheinlicher, dass ein Data Race einen Funktionsfehler auslöst. Wenn die problematischen Speicherzugriffe beispielsweise nur wenige Instruktionen nach dem Start des Threads vorkommen, ist sehr unwahrscheinlich, dass der Scheduler so früh einen Kontextwechsel vornimmt. Dementsprechend gering ist das Risiko, dass die Zugriffe falsch verzahnt werden. Bei der feinstufigeren Verzahnung auf einem Multi-Core-Prozessor ist dagegen schon aufgrund der größeren Zahl der Verzahnungen das Risiko eines Fehlers höher. Hieraus kann das Fazit gezogen werden, dass die einwandfreie Verarbeitung auf einem Single-Core-Prozessor denkbar schlecht geeignet ist, Aussagen über die Betriebssicherheit nebenläufiger Programme einzuholen. Latente, nicht zum Tragen gekommene Nebenläufigkeits-Schwachstellen, die sich bisher einer Entdeckung entzogen, können jetzt gravierende Fehler auslösen.

Traditionelle Prüftechniken unzureichend

Traditionelle Prüftechniken sind nur unzureichend in der Lage, Nebenläufigkeits-Defekte zu finden. Entwickler müssen deshalb neue Methoden und Werkzeuge einsetzen, um sichere Aussagen über die einwandfreie Verarbeitung von Multi-Core-Software zu bekommen. Inzwischen werden dynamische Tools verfügbar, die entweder die Verarbeitung des Codes auf verdächtige Speicherzugriffs-Muster hin absuchen oder aber den Prüfern die Möglichkeit geben, das Scheduling genau zu kontrollieren, damit sich Fehler leichter reproduzieren und diagnostizieren lassen.

Statistische Analyse

Statische Methoden hingegen bieten auf andere Weise einen Nutzen, denn sie decken etwaige Defekte auf, indem sie den Quellcode direkt auf Verarbeitungspfade absuchen, die unter Umständen problematisch sein könnten. Damit vermeiden die statischen Tools die Notwendigkeit, die enorme Vielfalt möglicher Thread-Verzahnungen zu durchsuchen. Entwickler, die Software von hoher Qualität hervorbringen wollen, sind aus diesem Grund gut beraten, bei der Suche nach Nebenläufigkeits-Fehlern und bei deren Beseitigung sowohl auf statische als auch auf dynamische Tools zu setzen.

GrammaTech Inc.
www.grammatech.com

Das könnte Sie auch Interessieren