C++: Automatische Erkennung von Data Races

von Hubert Schmid vom 2014-03-16

Das Speichermodell von C++ ist eigentlich ganz einfach. Enthält das Programm keine Data Races, so ist die Ausführung garantiert sequentially consistent. In allen anderen Fällen ist das Ausführungsverhalten dagegen undefiniert.

Trotz dieser einfachen Regel sieht man immer wieder Quellcode, der sich nicht daran hält. Zu den bekannteren Beispielen gehören sicherlich zahlreiche Implementierungen des sogenannten Double-checked Locking. Das folgende Listing zeigt so eine fehlerhafte Implementierung.

std::mutex mutex; thing* instance{nullptr}; auto get_thing() -> thing* { if (instance == nullptr) { std::lock_guard<std::mutex> lock{mutex}; if (instance == nullptr) { instance = new thing; } } return instance; }

Häufig ist es sehr anstrengend, Entwicklern das Problem begreiflich zu machen. Doch glücklicherweise gibt es mittlerweile Unterstützer mit großer Überzeugungskraft: Sowohl GCC als auch Clang können nämlich viele der problematischen Fälle automatisch erkennen und darauf hinweisen. Dafür muss man das Problem lediglich mit den Optionen -fsanitize=thread -fPIE -pie übersetzen (seit GCC 4.8 und Clang 3.2). Auf diese Weise instrumentierte Programme warnen zur Laufzeit vergleichsweise zuverlässig vor problematischen Speicherzugriffen. Die Ausgabe von Clang sieht beispielsweise (gekürzt) wie folgt aus:

WARNING: ThreadSanitizer: data race (pid=15443) Read of size 8 by thread T1: #0 get_thing() /tmp/demo.cpp:12 Previous write of size 8 by main thread (mutexes: write M4): #0 get_thing() /tmp/demo.cpp:15 SUMMARY: ThreadSanitizer: data race /tmp/demo.cpp:12 get_thing()

Korrigieren lässt sich obige Implementierung durch Verwendung von std::atomic. Die Definition von instance sollte wie folgt aussehen. Die Eliminierung der Data Race wird durch das Ausbleiben obiger Warnung von GCC und Clang bestätigt.

std::atomic<thing*> instance{nullptr};

Die Option -fsanitize=thread verursacht einen signifikanten Overhead während der Programmausführung. Üblicherweise ist er niedrig genug, um die Anwendungen noch sinnvoll testen zu können. Für den produktiven Einsatz ist die Option jedoch ungeeignet. Empfehlenswert ist dagegen die Einbindung in automatisierte Integrationstests – zumindest für Projekte, in denen hinreichend häufig Probleme dieser Art auftreten. Denn solche Maßnahmen können die Qualität deutlich und nachhaltig verbessern.