Concurrency-Probleme Projekte parallel programmieren

Multicore-Prozessor-Plattformen sind anfällig für Concurrency-Fehler, die aus einem nicht synchronisierten Zugriff auf einen gemeinsam genutzten Speicher entstehen.

Bild: GrammaTech
17.10.2016

Zukünftig werden Embedded-Projekte Java und C oder C++ beinhalten. Damit Entwickler dies gleichzeitig programmieren können, müssen Concurrency-Probleme ausgeschlossen werden. Mit statischen Analyse-Tools können die Codes auf gängige Fehler untersucht werden.

In Multicore-Plattformen erfolgt die Kommunikation zwischen Threads (Aktivitätsträger) oft über einen gemeinsam genutzten Speicher. Dadurch erreicht die Concurrency – nebenläufiges und paralleles Programmieren – neue Dimensionen. Multicore-Architekturen steigern den Durchsatz enorm, bringen aber auch große Komplexität mit, die wiederum einer neuen Klasse an besonders üblen Concurrency-Fehlern die Tür öffnet. Dazu zählen Race-Conditions, Deadlocks und Livelocks, die schon schwer zu finden sind, wenn sie auftreten. Noch schwieriger ist ihre Diagnose. Sie verlangen einen neuen Verifizierungsansatz speziell für Concurrency-Fehler.

Bei Embedded-Programmierern wird Java als Programmiersprache immer beliebter. Es belegt hinter C und C++ den dritten Platz mit einer Nutzungsquote von knapp unter 30 Prozent. Java verfügte stets über eine integrierte Unterstützung für Multithreading innerhalb der Syntax der Programmiersprache, Source-Compiler und Standardbibliotheken. Darüber hinaus hat Java 5 die Bibliothek java.util.concurrent hinzugefügt, die in Java 6 und Java 7 erweitert wurde, um einen umfassenden Support für nebenläufiges und paralleles Programmieren anzubieten.

Concurrency-Fehler in Java-Programmen

Durch die Multicore-Architekturen ist das Programmieren eines korrekten Concurrent-Java-Programms trotzdem schwierig und äußerst diffizil. Frühere Prozessor-Generationen setzten auf die schrittweise Steigerung bei der Geschwindigkeit der Prozessor-Clock, um die Leistung hochzutreiben. Demgegenüber nutzen Multicore-Prozessoren parallele Prozessoren und größere Nebenläufigkeit, um völlig skalierbare Leistung zu erzielen. Hier dürfen die Cores auch direkt über die gemeinsamen Hardwarespeicher kommunizieren, um so bessere Leistung und Ressourcenzuordnung zu erreichen, was zu noch höherer Programmierkomplexität führt.

Die Hauptfehlerquelle bei Multithread-Programmen ist die relative Reihenfolge, in der Befehle in Echtzeit ausgeführt werden. Beim Ablauf von Multithreads ändert sich die relative Reihenfolge der Befehlsausführung abhängig davon, welche der anderen Threads zur gleichen Zeit aktiv sind. Werden Bugs durch Programmierfehler eingeschleust, kann ein nicht-deterministisches Verzahnen zu unvorhersehbaren Ergebnissen führen. Mit der Anzahl an möglichen Interleavings (Verzahnungen) steigt die Anzahl der Befehle erheblich, bekannt ist das auch als „kombinatorische Explosion“.

Weil Echtzeit-Concurrency-Programme eine astronomische Anzahl an gültigen Interleavings aufweisen, wird das Testen jeder Verzahnung schlichtweg unzumutbar. Gleichermaßen ist es unmöglich, jeden potenziellen Ausführungspfad unter Verwendung von Peer-Code-Reviews oder Walkthroughs zu untersuchen. Genau hier spielen hoch entwickelte statische Analysetools ihre Stärke aus.

Checker prüfen den Code auf Fehler

Mithilfe von symbolischen Ausführungs-Engines identifizieren die Analysetools mögliche Probleme bei einem Programm, ohne dass sie dieses tatsächlich ausführen müssen. Ihre Funktionsweise ähnelt sehr den Compilern, wobei sie Quellcode als Input verwenden, diesen dann parsen und ihn in eine Zwischendarstellung (IR, Intermediate Representation) umwandeln. Während ein Compiler mit der IR Objektcode generieren würde, behalten statische Analyse-Tools die IR – auch Modell genannt – weiterhin bei.

Checker prüfen mit Analysen den Code, um unter anderem gängige Fehler, Verletzungen von Richtlinien aufzuspüren. Dazu durchlaufen sie das Modell oder untersuchen es nach bestimmten Eigenschaften oder Mustern, die auf Fehler hinweisen. Hoch entwickelte symbolische Ausführungstechniken durchsuchen Pfade mithilfe eines Kontrollfluss-Diagramms – einer Daten-
struktur, die die Reihenfolge darstellt, der Anweisungen während der Ausführung eines Programms folgen. Algorithmen verfolgen den abstrakten Programmzustand und verstehen es, diesen zum Ausschließen von unmöglichen Pfaden einzusetzen. Die Tiefe des Modells entscheidet über die Effizienz des Tools. Diese Tiefe basiert darauf, wie viel Wissen zum Programmverhalten integriert ist, wie groß der Teil des Programms ist, den es auf einmal berücksichtigen kann, und wie genau es ein tatsächliches Programmverhalten widerspiegelt.

Viele Entwickler setzen auf gängige Open-Source-Tools, einschließlich FindBugs, PMD und CheckStyle, um Fehler in Java aufzufinden. Jedes dieser Tools hat seine Stärken. Beispielsweise nutzt FindBugs statische Analysen, um hunderte von verschiedenen Arten von möglichen Fehlern in Java-Programmen zu identifizieren. Es läuft auf Java-Bytecode, also auf Befehlen, die die virtuelle Maschine von Java ausführt. PMD und Check-
Style überprüfen Quellcode hinsichtlich Codierungsstandards und spüren Unregelmäßigkeiten auf. Demgegenüber bieten statische Analysetools generell den wichtigen Vorteil, dass sie schon früh in der Entwicklung verwendet werden können, um Fehler noch vor Testbeginn zu finden. Die meisten für Java erhältlichen statischen Analysetools sind universell einsetzbar und erfassen eine Reihe an Problemen in der Oberfläche.

Im Vergleich zu den genannten Open-Source-Tools gibt es kommerzielle Produkte, die für eine sehr genaue Untersuchung von Nebenläufigkeitsproblemen in Java, C oder C++ maßgeschneidert sind. Diese Tools beinhalten tiefgehende Modelle, mit denen sie Concurrency-Probleme auffinden können, die bei anderen Tools häufig übersehen werden. Einige der effizientesten statischen Analyse-Tools basieren auf akademischer Forschung auf dem Gebiet des Concurrency-Verhaltens von Software. Sie bieten moderne statische Analysen von Quellcode in C und C++ mit interprozeduraler Analyse eines gesamten Programms und können typischerweise sehr große Programme mit bis zu zehn Millionen Codezeilen bearbeiten. Zusätzlich zum Auffinden von Race-Conditions und Deadlocks identifiziert eines der kommerziellen Tools für Java unvorhersehbare Ergebnisse, die durch eine fehlerhafte Verwendung der parallelen, von java.util.concurrent zur Verfügung gestellten Sammelbibliotheken auftreten. Es spürt schlechte Fehlerbehandlung oder eine unsachgemäße Synchronisierung beim Koordinieren des Zugriffs auf gemeinsam genutzte, nicht parallele Sammlungen auf. Außerdem kann es bei der Diagnose von Leistungsengpässen durch eine fehlerhafte API-Nutzung (Application Programming Interface), redundante Synchronisation und unnötigen Einsatz eines gemeinsamen veränderlichen Zustands unterstützen.

In integrierter Entwicklungsumgebung arbeiten

Die Entwicklung von Embedded-Anwendungen für Multicore-Plattformen erfordert einen neuen Ansatz. Nur die statische Analyse allein bietet ein Mittel, um alle möglichen Codepfade für Softwarefehler in Systemen mit hoher Concurrency zu prüfen. Werden moderne statische Analyse-Tools mit anderen Praktiken zur Prüfung der Codequalität (zum Beispiel Code-Reviews, Integrationstests) kombiniert, können sie das Risiko von Ausfällen aufgrund unentdeckter Concurrency-Fehler reduzieren.

Firmen zu diesem Artikel
Verwandte Artikel