Gewindesicherheit - Thread safety
Thread-Sicherheit ist ein Computerprogrammierungskonzept, das auf Multithread- Code anwendbar ist . Thread-sicherer Code manipuliert gemeinsam genutzte Datenstrukturen nur so, dass sichergestellt ist, dass sich alle Threads ordnungsgemäß verhalten und ihre Entwurfsspezifikationen ohne unbeabsichtigte Interaktion erfüllen. Es gibt verschiedene Strategien, um threadsichere Datenstrukturen zu erstellen.
Ein Programm kann Code in mehreren Threads gleichzeitig in einem gemeinsam genutzten Adressraum ausführen, in dem jeder dieser Threads Zugriff auf praktisch den gesamten Speicher jedes anderen Threads hat. Die Thread-Sicherheit ist eine Eigenschaft, mit der Code in Multithread-Umgebungen ausgeführt werden kann, indem einige der Entsprechungen zwischen dem tatsächlichen Steuerungsfluss und dem Programmtext durch Synchronisierung wiederhergestellt werden .
Gewindesicherheitsstufen
Softwarebibliotheken können bestimmte Thread-Sicherheitsgarantien bieten. Beispielsweise kann garantiert werden, dass gleichzeitige Lesevorgänge threadsicher sind, gleichzeitige Schreibvorgänge jedoch möglicherweise nicht. Ob ein Programm, das eine solche Bibliothek verwendet, threadsicher ist, hängt davon ab, ob es die Bibliothek in einer Weise verwendet, die diesen Garantien entspricht.
Verschiedene Anbieter verwenden aus Gründen der Thread-Sicherheit leicht unterschiedliche Begriffe:
- Thread-sicher : Die Implementierung ist garantiert frei von Race-Bedingungen, wenn mehrere Threads gleichzeitig darauf zugreifen.
- Bedingt sicher : Verschiedene Threads können gleichzeitig auf verschiedene Objekte zugreifen, und der Zugriff auf gemeinsam genutzte Daten ist vor Rennbedingungen geschützt.
- Nicht threadsicher : Auf Datenstrukturen sollte nicht gleichzeitig von verschiedenen Threads zugegriffen werden.
Zu den Gewindesicherheitsgarantien gehören normalerweise auch Entwurfsschritte, um das Risiko verschiedener Arten von Deadlocks zu verhindern oder zu begrenzen , sowie Optimierungen, um die gleichzeitige Leistung zu maximieren. Deadlock-freie Garantien können jedoch nicht immer gegeben werden, da Deadlocks durch Rückrufe und Verstöße gegen die Architekturschicht unabhängig von der Bibliothek selbst verursacht werden können.
Implementierungsansätze
Nachfolgend diskutieren wir zwei Klassen von Ansätzen zur Vermeidung von Rennbedingungen , um Fadensicherheit zu erreichen.
Die erste Klasse von Ansätzen konzentriert sich auf die Vermeidung eines gemeinsamen Zustands und umfasst:
- Wiedereintritt
- Schreiben von Code so, dass er teilweise von einem Thread ausgeführt, von demselben Thread ausgeführt oder gleichzeitig von einem anderen Thread ausgeführt werden kann und die ursprüngliche Ausführung dennoch korrekt abgeschlossen werden kann. Dies erfordert die Speicherung von Zustandsinformationen in Variablen lokal für jede Ausführung, in der Regel auf einem Stapel, statt in statischen oder globalen Variablen oder andere nicht-lokalen Zustand. Auf alle nicht lokalen Zustände muss über atomare Operationen zugegriffen werden, und die Datenstrukturen müssen auch wiedereintrittsfähig sein.
- Thread-lokaler Speicher
- Variablen werden so lokalisiert, dass jeder Thread eine eigene private Kopie hat. Diese Variablen behalten ihre Werte über Unterroutinen und andere Codegrenzen hinweg bei und sind threadsicher, da sie für jeden Thread lokal sind, obwohl der Code, der auf sie zugreift, möglicherweise gleichzeitig von einem anderen Thread ausgeführt wird.
- Unveränderliche Gegenstände
- Der Zustand eines Objekts kann nach der Erstellung nicht mehr geändert werden. Dies impliziert sowohl, dass nur schreibgeschützte Daten gemeinsam genutzt werden, als auch, dass die inhärente Thread-Sicherheit erreicht wird. Veränderbare (nicht konstante) Operationen können dann so implementiert werden, dass sie neue Objekte erstellen, anstatt vorhandene zu ändern. Dieser Ansatz ist charakteristisch für die funktionale Programmierung und wird auch von den String- Implementierungen in Java, C # und Python verwendet. (Siehe Unveränderliches Objekt .)
Die zweite Klasse von Ansätzen bezieht sich auf die Synchronisation und wird in Situationen verwendet, in denen ein gemeinsamer Zustand nicht vermieden werden kann:
- Gegenseitiger Ausschluss
- Der Zugriff auf gemeinsam genutzte Daten wird mithilfe von Mechanismen serialisiert , die sicherstellen, dass jeweils nur ein Thread die gemeinsam genutzten Daten liest oder schreibt. Die Einbeziehung des gegenseitigen Ausschlusses muss gut durchdacht sein, da eine unsachgemäße Verwendung zu Nebenwirkungen wie Deadlocks , Livelocks und Ressourcenmangel führen kann .
- Atomoperationen
- Auf gemeinsam genutzte Daten wird mithilfe von atomaren Operationen zugegriffen, die nicht von anderen Threads unterbrochen werden können. Dies erfordert normalerweise die Verwendung spezieller Anweisungen in Maschinensprache , die möglicherweise in einer Laufzeitbibliothek verfügbar sind . Da die Operationen atomar sind, werden die gemeinsam genutzten Daten immer in einem gültigen Zustand gehalten, unabhängig davon, wie andere Threads darauf zugreifen. Atomoperationen bilden die Grundlage vieler Thread-Verriegelungsmechanismen und werden verwendet, um Grundelemente für den gegenseitigen Ausschluss zu implementieren.
Beispiele
Im folgenden Java- Code macht das synchronisierte Java-Schlüsselwort die Methode threadsicher:
class Counter {
private int i = 0;
public synchronized void inc() {
i++;
}
}
In der Programmiersprache C hat jeder Thread seinen eigenen Stapel. Eine statische Variable wird jedoch nicht auf dem Stapel gespeichert. Alle Threads haben gleichzeitig Zugriff darauf. Wenn sich mehrere Threads überlappen, während dieselbe Funktion ausgeführt wird, kann es sein, dass eine statische Variable von einem Thread geändert wird, während sich ein anderer in der Mitte der Überprüfung befindet. Dieser schwer zu diagnostizierende Logikfehler , der die meiste Zeit ordnungsgemäß kompiliert und ausgeführt werden kann, wird als Race-Bedingung bezeichnet . Ein üblicher Weg , dies zu vermeiden , ist eine weitere gemeinsame Variable als zu verwenden , „Lock“ oder „Mutex“ (von mut ual ex schluss).
Im folgenden Teil des C-Codes ist die Funktion threadsicher, aber nicht wiedereintrittsfähig:
# include <pthread.h>
int increment_counter ()
{
static int counter = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// only allow one thread to increment at a time
pthread_mutex_lock(&mutex);
++counter;
// store value before any other threads increment it further
int result = counter;
pthread_mutex_unlock(&mutex);
return result;
}
Oben increment_counter
kann von verschiedenen Threads problemlos aufgerufen werden, da ein Mutex verwendet wird, um den gesamten Zugriff auf die gemeinsam genutzte counter
Variable zu synchronisieren . Wenn die Funktion jedoch in einem wiedereintretenden Interrupt-Handler verwendet wird und ein zweiter Interrupt auftritt, während der Mutex gesperrt ist, bleibt die zweite Routine für immer hängen. Da die Interrupt-Wartung andere Interrupts deaktivieren kann, kann das gesamte System darunter leiden.
Dieselbe Funktion kann mithilfe der sperrfreien Atomics in C ++ 11 implementiert werden, um sowohl threadsicher als auch wiedereintrittsfähig zu sein :
# include <atomic>
int increment_counter ()
{
static std::atomic<int> counter(0);
// increment is guaranteed to be done atomically
int result = ++counter;
return result;
}
Siehe auch
Verweise
Externe Links
- Java Q & A Experts (20. April 1999). "Gewindesicheres Design (20.04.1999)" . JavaWorld.com . Abgerufen am 22.01.2012 .
- TutorialsDesk (30. September 2014). "Tutorial zur Synchronisation und Thread-Sicherheit mit Beispielen in Java" . TutorialsDesk.com . Abgerufen am 22.01.2012 .
- Venners, Bill (1. August 1998). "Design für Gewindesicherheit" . JavaWorld.com . Abgerufen am 22.01.2012 .
- Suess, Michael (15. Oktober 2006). "Eine kurze Anleitung zur Beherrschung der Thread-Sicherheit" . Parallel denken . Abgerufen am 22.01.2012 .