Gliederung 1. Einführung und Übersicht 2. Prozesse und Threads 3. Interrupts 4. Scheduling Synchronisation 5. Synchronisation 6. Interprozesskommunikation 7. Speicherverwaltung BS-I / Cl. Schnörr / HM Synchronisation 1 BS-I / Cl. Schnörr / HM Gliederung Synchronisation 2 Einführung (1) Wozu Synchronisation ? Übersicht: Einführung Threads und z.T. Prozesse haben gemeinsamen Zugriff auf bestimmte Daten, z.B. Threads des gleichen Prozesses: Race-Condition gemeinsamer virtueller Speicher Kritische Abschnitte öffnen der gleichen Datei zum Lesen/Schreiben Mechanismen des BS und Standard-Primitive Prozesse mit Shared-Memory (--> IPC) Tips zur praktischen Anwendung: SMP-System: Scheduler (je einer pro CPU): Deadlocks vermeiden Mutex-Objekt Zugriff auf gleiche Prozesslisten / Warteschlangen Datenbanken: Zugriff über eine globale Datenbank-Verbindung (DB-Connect) BS-I / Synchronisation Cl. Schnörr / HM 3 BS-I / Synchronisation Cl. Schnörr / HM 4 Einführung (2) Einführung (3) Beispiel: gleichzeitiger Zugriff auf Datenstruktur: Beispiel: gleichzeitiger Zugriff auf Datenstruktur: zwei Threads erhöhen einen gemeinsamen Zähler: gewünscht: eine der folgenden koordinierten Reihenfolgen: BS-I / Cl. Schnörr / HM Synchronisation 5 BS-I / Synchronisation Einführung (4) Beispiel: Datenbanken: Ursache: Datenbank: analoges Problem: Cl. Schnörr / HM 8 exec sql CONNECT ... exec sql SELECT kontostand INTO $var FROM KONTO WHERE kontonummer = $knr $var = $var - abhebung exec sql UPDATE Konto SET kontostand = $var WHERE kontonummer = $knr exec sql disconnect erhoehe_zaehler() arbeitet nicht atomar: Scheduler kann Funktion unterbrechen (anderer Thread arbeitet weiter) Funktion kann auf mehreren CPUs gleichzeitig laufen Lösung: stelle sicher, dass paralleler Zugriff auf gleichen Datensatz potentiell fehlerhaft immer nur ein Prozess/Thread gleichzeitig auf gemeinsame Daten zugreift --> Definition der (Datenbank-) Transaktion, die bis Vorgang abgeschlossen ist u.a. atomar und isoliert erfolgen muss == gegenseitiger Ausschluß (Mutual Exclusion) Synchronisation 6 Einführung (5) Beispiel: gleichzeitiger Zugriff auf Datenstruktur: BS-I / Cl. Schnörr / HM Cl. Schnörr / HM 7 BS-I / Synchronisation Race-Condition (1) Race-Condition (2) Eigenschaften: Race-Condition als Sicherheitslücke Race Condition Race-Conditions sind auch Sicherheitslücken mehrere parallele Threads/Prozesse nutzen gemeinsame Ressource wird vom Angreifer genutzt Zustand hängt von Reihenfolge der Ausführung ab: einfaches Beispiel: Ergebnis nicht vorhersagbar / nicht reproduzierbar read( command ); f = open( “/tmp/script“, “w“ ); write( f, command ); close( f ); chmod( “/tmp/script“, “a+x“ ); system( “/tmp/script“ ); Race: Threads liefern sich ein „Rennen“ um den ersten/schnellsten Zugriff Warum Race-Conditions vermeiden? Ergebnisse paralleler Berechnungen sind potentiell falsch Angreifer ändert Dateiinhalt vor dem chmod() Programmtests können „funktionieren“, später die Anwendung aber versagen BS-I / Cl. Schnörr / HM Synchronisation / Race-Condition --> Programm läuft mit Rechten des Opfers 9 BS-I / Synchronisation / Race-Condition Race-Condition (3) Zugriff via Flag / Lock auf Thread/Prozess beschränken: erhoehe_zaehler() { flag = read( lock ); if ( flag == LOCK_NOT_SET ) { set( lock ); //Start kritischer Bereich w = read( adr ); w = w + 1; write( adr, w ); //Ende kritischer Bereich release( lock ); } } Problem: Lock-Variable nicht geschützt BS-I / Synchronisation / Race-Condition 10 Kritischer Bereich Was ist ein „kritischer Bereich“ ? Aspekte einer Lösung Idee: Cl. Schnörr / HM kein Problem: Programmteil, der auf gemeinsame Daten zugreift gleichzeitiges Lesen von Daten Block zwischen erstem und letztem Zugriff Threads, die “disjunkt“ sind, d.h. Formulierung: kritischen Bereich betreten / verlassen (enter / leave critical section) keine gemeinsame Daten haben / nutzen Anforderungen an parallele Threads: problematisch: maximal ein Thread gleichzeitig in kritischem Bereich mehrere Prozesse/Threads kein Thread außerhalb kritischem Bereich darf anderen blockieren (--> potentiell Deadlock) greifen gemeinsam auf Objekt zu, kein Thread soll ewig auf Freigabe eines kritischen Bereichs warten davon mindestens einer schreibend Deadlocks zu vermeiden, z.B. zwei Threads in verschiedenen kritischen Bereichen blockieren sich gegenseitig Cl. Schnörr / HM 11 BS-I / Synchronisation / kritischer Bereich Cl. Schnörr / HM 12 Gegenseitiger Ausschluß Programmtechnische Synchronisation Gegenseitiger Ausschluß (Mutual Exclusion, kurz Mutex): Idee war: nie mehr als ein Thread betritt kritischen Bereich Zugriff via Flag / Lock auf Thread/Prozess beschränken es ist Aufgabe des Programmierers, dies zu garantieren Problem: das BS bietet Mechanismen und Hilfsmittel, gegenseitigen Ausschluß durchzusetzen Lock-Variable nicht geschützt Verantwortung für Fehlerfreiheit liegt beim Programmierer: Lösung: Compiler können i.d.R. Fehler aus Nebenläufigkeit nicht erkennen eine Lock-Variable atomar testen und setzen dies kann per Programmlogik über mehrere Variable erreicht werden (--> Lit.: Dekker1966, Petersons Algorithmus) ist kompliziert bei mehr als zwei Threads/Prozessen besser: Nutzung von „standard“ BS-Mechanismen BS-I / Cl. Schnörr / HM Synchronisation / Mutex 13 BS-I / Cl. Schnörr / HM Synchronisation 14 Test-and-Set-Lock (TSL) Maschineninstruktion (moderner CPUs) mit dem Namen TSL = Test and Set Lock die atomar eine Lock-Variable liest (testet) und setzt, also garantiert ohne Unterbrechung im Fall mehrerer CPUs: Synchronisation TSL muss Speicherbus sperren, damit kein Thread auf anderer CPU in gleicher Weise zugreifen kann Mechanismen und Standard-Primitive enter: tsl register, flag cmp register, 0 jnz enter ret leave: mov flag, 0 ret BS-I / Synchronisation / Mechanismen Cl. Schnörr / HM 15 BS-I / ; ; ; ; Variable in Register kopieren und dann Variable auf 1 setzen war Variable 0 ? nicht 0: Lock war gesetzt, also Schleife(pollen) ; 0 in flag speichern: Lock freigeben Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 16 Aktives und passives Warten (1) Aktives und passives Warten (2) Aktives Warten (busy waiting): Passives Warten (sleep and wake): Ausführen einer Schleife, bis eine Variable einen bestimmten Wert annimmt Thread blockiert und wartet auf Ereignis, das ihn in den Zustand „bereit“ versetzt Thread ist bereit und belegt die CPU blockierter Thread verschwendet keine CPU-Zeit Variable muss von einem anderen Thread gesetzt werden anderer Thread muss Eintreten des Ereignisses bewirken (kleines) Problem, wenn anderer Thread endet (großes) Problem, wenn der andere Thread endet bei Ereignis muss blockierter Thread geweckt werden (großes) Problem, wenn anderer Thread -- z.B. wegen niedriger Priorität -- nicht dazu kommt, Variable zu setzen explizit durch anderen Thread durch Mechanismen des BS auch Pollen/Polling genannt BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 17 BS-I / Synchronisation / Mechanismen+Primitive Erzeuger-Verbraucher-Problem (1) Cl. Schnörr / HM 18 Erzeuger-Verbraucher-Problem (2) Synchronisation: Erzeuger-Verbraucher-Problem (producer-consumer problem, bounded-buffer problem) Puffer nicht überfüllen: zwei kooperierende Threads: wenn Puffer voll, muß Erzeuger warten, bis Verbraucher ein weiteres Paket abgeholt hat Erzeuger speichert Informationspaket in beschränktem Puffer nicht aus leerem Puffer lesen: Verbraucher liest Informationen aus diesem Puffer wenn Puffer leer, muß Verbraucher warten, bis Erzeuger ein weiteres Paket abgelegt hat Realisierung mit passivem Warten: eine gemeinsame Variable „count“ zählt belegte Positionen im Puffer wenn Erzeuger ein Paket einstellt und Puffer leer war (count == 0) --> wecken des Verbrauchers wenn Verbraucher ein Paket abholt und Puffer voll war (count == max) --> wecken des Erzeugers BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 19 BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 20 Erzeuger-Verbraucher mit sleep-wake #define N 100 int count = 0; Deadlock-Problem bei sleep / wake (1) //Anzahl der Plätze im Puffer //Anzahl der belegten Plätze im Puffer Verbraucher liest Variable count, die den Wert 0 hat producer() { while (TRUE) { // Endlosschleife produce( item ); // Erzeuge etwas für den Puffer // Wenn Puffer voll: schlafen legen if (count == N) sleep(); enter( item ); // In den Puffer einstellen count = count + 1; //Zahl belegter Plätze inkrementieren if (count == 1) wake(consumer); //war der Puffer vorher leer? } } Kontextwechsel zum Erzeuger: Erzeuger stellt etwas in den Puffer, erhöht count und weckt Verbraucher, da count==0 war consumer() { while (TRUE) { // Endlosschleife // Wenn Puffer leer: schlafen legen if (count == 0) sleep(); remove_item (item); // Etwas aus dem Puffer entnehmen count = count - 1; // Zahl belegter Plätze dekrementieren if (count == N-1) wake(producer); //war der Puffer vorher voll? consume_item (item); // Verarbeiten } } BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM Race-Condition im vorigen Programm --> potentieller Deadlock, z.B. 21 Kontextwechsel zum Verbraucher: Verbraucher legt sich schlafen, da count==0 gelesen wurde Erzeuger schreibt Puffer voll und legt sich auch schlafen BS-I / Synchronisation / Mechanismen+Primitive Deadlock-Problem bei sleep / wake (2) Cl. Schnörr / HM 22 Cl. Schnörr / HM 24 Standardprimitive zur Synchronisation Ursache des Problems: Welche Standardprimitive zur Synchronisation gibt es ? Wakeup-Signal für einen -- noch nicht -- schlafenden Prozess wird ignoriert Mutex (mutual exclusion) = binärer Semaphor --> Weckaufruf „irgendwie“ aufbewahren Semaphor Lösungsmöglichkeit: Event (ähnlich Condition-Variable) Systemaufrufe sleep() und wake() verwenden „wakeup pending bit“ Monitor bei wake() für nicht schlafenden Thread dessen wakeup-pending-bit setzen Locking bei sleep() das wakeup-pending-bit des Threads prüfen und falls gesetzt, nicht schlafen legen aber: Lösung lässt sich nicht verallgemeinern (mehrere Prozesse benötigen weitere Bits). BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 23 BS-I / Synchronisation / Mechanismen+Primitive Mutex (1) Mutex (2) wait() und signal()-Operationen sind selbst kritische Abschnitte --> atomar realisiert Mutex (mutual exclusion) = binärer Semaphor Implementierung als System-Calls und Verhinderung von Kontext-Wechseln (z.B. durch kurzzeitiges Ausschalten von Interrupts) zur Synchronisation eines kritischen Bereichs bzw. gemeinsamer Daten kann nur 2 Zustände / Werte annehmen: Zugang erlaubt true / frei: false / gesperrt: Zugang gesperrt wait( mutex ) { if ( mutex == 1 ) mutex = 0; else BLOCK_CALLER; } Anforderungs- und Freigabe-Operationen: Anforderung: wait() / lock() / get() signal() / unlock() / release() Freigabe: Anforderung: ein Thread, der eine bereits vergebene Mutex anfordert, blockiert --> Warteschlange Freigabe: Warteschlange enthält Threads Warteschlange leer BS-I / --> einen wecken --> Mutex auf true Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 25 BS-I / Synchronisation / Mechanismen+Primitive Semaphor (1) 26 Cl. Schnörr / HM 28 Varianten: Integer- (Zähler-) Variable negative Semaphor-Werte: mit festgelegtem Anfangswert N („Anzahl verfügbarer Ressourcen“) Anforderung (wait()): - Wert immer um 1 reduzieren - --> Wert entspricht Zahl blockierter Threads in Warteschlange Anforderung (wait()): Wert um 1 reduzieren Thread blockieren --> Warteschlange Freigabe (signal()): - Wert immer um 1 erhöhen Freigabe (signal()): falls Warteschlange nicht leer: falls Warteschlange leer: nicht-blockierend: z.B. bool ret = try_lock(); einen Thread wecken Wert um 1 erhöhen pthreads-Semaphore: sind auch zwischen Prozessen verwendbar (Standard: nur Threads) SysV-IPC Semaphore: arbeiten prozessübergreifend sind nach Prozessende noch vorhanden wait( &sem ); // Kode, der die Ressource nutzt signal( &sem ); BS-I / Cl. Schnörr / HM Semaphor (2) Semaphor: falls >= 1: falls == 0: signal( mutex ) { if ( P in QUEUE(mutex)) { wakeup( P ); remove( P, QUEUE ); } else mutex = 1; } Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 27 BS-I / Synchronisation / Mechanismen+Primitive Semaphor (3) Erzeuger-Verbraucher-Problem mit Semaphoren Beispiel: Verbraucher: Threads A,B,C: wait() Erzeuger: signal() Thread D: typedef int semaphore; semaphore mutex = 1; semaphore empty = N; semaphore full = 0; Semaphore s zählt verfügbare Ressource BS-I / Cl. Schnörr / HM Synchronisation / Mechanismen+Primitive 29 // Kontrolliert Zugriff auf Puffer // Zählt freie Plätze im Puffer // Zählt belegte Plätze im Puffer producer() { while (TRUE) { produce_item(item); wait(empty); wait(mutex); enter_item(item); signal(mutex); signal(full); } } // // // // // // // Endlosschleife Erzeuge etwas für den Puffer Leere Plätze dekrementieren bzw. blockieren Eintritt in den kritischen Bereich In den Puffer einstellen Kritischen Bereich verlassen Belegte Plätze erhöhen, evtl. consumer wecken consumer() { while (TRUE) { wait(full); wait(mutex); remove_item(item); signal(mutex); signal(empty); consume_entry(item) } } // // // // // // // Endlosschleife Belegte Plätze dekrementieren bzw. blockieren Eintritt in den kritischen Bereich Aus dem Puffer entnehmen Kritischen Bereich verlassen Freie Plätze erhöhen, evtl producer wecken Verbrauchen BS-I / Cl. Schnörr / HM Synchronisation / Mechanismen+Primitive Events (1) 30 Events (2) Events: Beispiel: kein Thread besitzt einen Event (<-> Mutex) wait() blockiert, falls Event nicht im signalisierten Zustand automatischer Event: thread A . event.wait() . . thread B . . event.signal(); . manual automatic t # threads waiting after each call: jedes wait() setzt Event automatisch zurück (reset) 2 2 0 1 0 0 0 0 0 0 0 1 0 2 0 0 manual automatic signalled falls mehrere Threads warten: bei einem signal() kehrt nur genau ein Thread von seinem wait() zurück non-signalled Zeit t manueller Event: signal() signal() signal() signal() wait() wait() reset() Event muß manuell zurückgesetzt werden (reset) falls mehrere Threads warten: bei einem signal() kehren alle Threads von ihren wait() zurück BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 31 BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 32 Monitor (1) Monitor (2) Problem bei Arbeit mit Mutexen und Semaphoren: Programmierer gezwungen, kritische Bereiche mit wait() und signal() abzusichern schon bei einem Fehler --> Synchronisierung versagt Monitor: muss von Programmiersprache unterstützt werden (z.B. Java, Concurrent-Pascal) Sammlung von Prozeduren, Variablen, speziellen Bedingungsvariablen: Prozesse können Prozeduren des Monitors aufrufen, aber nicht auf dessen Datenstrukturen zugreifen kapselt kritische Bereiche zu jedem Zeitpunkt nur ein einziger Prozess im Monitor aktiv (Monitor-Prozedur ausführen) Freigabe durch Verlassen der Monitor-Prozedur BS-I / Cl. Schnörr / HM Synchronisation / Mechanismen+Primitive 33 BS-I / Synchronisation / Mechanismen+Primitive Monitor (3) Monitor-Konzept erinnert an Kapselung: nur Zugriff über public-Prozeduren wait( disc_access ); // Daten lesen signal( disc_access ); 36 Was tun, wenn Prozess im Monitor blockieren muss ? Condition-Variablen (Zustandsvariable): gleiches Beispiel: mit Monitor: Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM „Klasse“, bei der jede public-Methode synchronisiert ist wait( disc_access ); // Daten schreiben signal( disc_access ); BS-I / Monitor wird von Programmiersprache / Compiler bereitgestellt nicht der Programmierer ist für gegenseitigen Ausschluß verantwortlich Beispiel: Zugriff auf Festplatte mit Mutex: monitor disc { entry read( discaddr, memaddr ) { // Daten lesen }; entry write( discaddr, memaddr ) { //Daten schreiben }; init() { // Gerät initialisieren }; }; 34 Monitor (4) Klassen (Objektorientierung) Module mutex disc_access = 1; Cl. Schnörr / HM Idee: Prozess muss auf Eintreten einer Bedingung (Condition) warten monitor disc; m_wait( var ): aufrufenden Prozess sperren (er gibt den Monitor frei) disc.read( da, ma ); m_signal( var ): disc.write( da, ma ); gesperrte(n) Prozess(e) wecken erfolgt unmittelbar vor Verlassen des Monitors Cl. Schnörr / HM 35 BS-I / Synchronisation / Mechanismen+Primitive Monitor (5) Process 1 Monitor Monitor (6) Prozess 2 Gesperrte Prozesse landen in Warteschlange zu entsprechender Condition-Variable Interne Warteschlangen haben Vorrang vor Prozesszugriffen von außen Daten ConditionVariable Prozedur Prozedur BS-I / f() cv Implementierung mit Mutex / Semaphor: g() f() { wait( cv); ... } m_wait(cv) Zeit t conditionVariable { g() { ... signal( cv); } Synchronisation / Mechanismen+Primitive wait() { m.lock(); queueSize++; m.release(); waiting.down(); } Cl. Schnörr / HM 37 BS-I / 38 Locking erweitert Funktionalität von Mutexen durch Unterscheidung verschiedener Lock-Modi (Zugriffsarten) und Festlegung derer „Verträglichkeit“ entry append( item x ) { while (count == N-1) m_wait(nonfull); put(buffer, x); // count += 1; m_signal(nonempty); } Concurrent Read: Lesezugriff, andere Schreiber erlaubt entry remove(item x) { while (count == 0) m_wait(nonempty); get(buffer, x); // count -= 1; m_signal(nonfull); } // Initialisierung } z.B. auf oberen Lock-Hierarchien in Datenbanken Concurrent Write: Schreibzugriff, andere Schreiber erlaubt Protected Read: Lesezugriff, andere Leser erlaubt, aber keine Schreiber (share lock) Protected Write: Schreibzugriff, andere Leser erlaubt, aber kein weiterer Schreiber (update lock) Exclusive: Lese/Schreibzugriff, keine anderen Zugriffe erlaubt Thread fordert Lock in bestimmtem Modus an. Ist Lock-Modus zu einem Lock eines anderen Threads unverträglich -> Blockierung } Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM Locking (1) monitor iostream { item buffer; int count; const int N = 64; condition nonempty, nonfull; BS-I / }; Synchronisation / Mechanismen+Primitive Erzeuger-Verbraucher-Problem mit Monitor init() { count = 0; } signal() { m.lock(); while( queueSize > 0 ) { // alle wecken queueSize--; waiting.up(); } m.release(); } int queueSize = 0; mutex m; semaphore waiting; m_signal(cv) Cl. Schnörr / HM 39 BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM 40 Locking (2) Lock-“Verträglichkeiten“: concurrent concurrent protected read write read protected exclusive write access sharing concurrent read read write + + + + - concurrent write write write + + - - - protected read read read + - + - - protected write write read + - - - - exclusive write -- - - - - - BS-I / Synchronisation / Mechanismen+Primitive Cl. Schnörr / HM Praxisbeispiele 41 BS-I / Praxis: Übersicht Typ atomic_t (24 Bit Integer): Initialisierung: atomic_t var = ATOMIC_INIT( 0 ); Atomare Operationen auf Integer-Variablen Bit-Operationen auf Bitvektoren Spin-Locks Reader-Writer-Locks Wert setzen: atomic_set( &var, wert ); Addieren: atomic_add( wert, &var ); ++: atomic_inc( &var ); Subtrahieren: atomic_sub( wert, &var ); Semaphore / Reader-Writer Semaphore Synchronisation in C++ Threads, Mutexe atomare Befehle Mutex-Objekt in C++ Synchronisation / Praxis 42 Praxis / Linux-Kernel: Atomare Integer-Operationen Synchronisation im Linux-Kernel BS-I / Cl. Schnörr / HM Synchronisation / Praxis --: atomic_dec( &var ); Auslesen: int i = atomic_read( &var ); res = atomic_sub_and_test( i, &var ); subtrahiert i atomar von var return true, falls Ergebnis 0, sonst false Cl. Schnörr / HM 43 BS-I / Synchronisation / Praxis / Linux-Kernel res = atomic_add_negative( i, &var ); addiert i atomar zu var return true, falls Ergebnis negativ, sonst false Cl. Schnörr / HM 44 Praxis / Linux-Kernel: Atomare Bit-Operationen Praxis / Linux-Kernel: Spin-Locks (1) Einzelne Bits in Bitvektoren setzen Spin-Lock: Datentyp: beliebig, z.B. unsigned long bitvektor = 0; Lock mit Mutex-Funktion: gegenseitiger Ausschluß nur über Pointer anzusprechen Anzahl der nutzbaren Bits abhängig vom verwendeten Datentyp Test-and-Set-Operationen geben zusätzlich vorherigen Wert des jeweiligen Bits zurück set_bit( i, &bv ); clear_bit( i, &bv ); change_bit( i, &bv ); i-tes Bit setzen i-tes Bil löschen i-tes Bit kippen Code, der Spin-Lock anfordert und nicht erhält blockiert nicht (kein aufwendiger Wechsel in Kernel-Mode) sondern läuft weiter (spinning), bis Lock verfügbar b = test_and_set_bit( i, &bv ); b = test_and_clear_bit( i, &bv ); b = test_and_change_bit( i, &bv ); nur zu verwenden bei kurzen „Wartezeiten“ auf Lock, bei Mehrprozessorsystemen Einzelne Bits auslesen: b = test_bit( i, &bv ); sind nicht „rekursiv“, also z.B. nicht in rekursiven Funktionen verwendbar Suchfunktionen: Typ spinlock_t spin_lock( &slock ): /* kritischer Abschnitt */ spin_unlock( &slock ); pos = find_first_bit( &bv, length ); pos = find_first_zero_bit( &bv, length ); BS-I / Cl. Schnörr / HM Synchronisation / Praxis / Linux-Kernel 45 BS-I / Praxis / Linux-Kernel: Spin-Locks (2) Cl. Schnörr / HM Synchronisation / Praxis / Linux-Kernel 46 Praxis / Linux-Kernel: Reader-Writer-Locks da Spin-Locks nicht schlafen/blockieren, sind diese in Interrupt-Handlern verwendbar Reader-Writer-Locks: in diesem Fall: zusätzlich Interrupts sperren: Alternative zu normalen Locks, die mehrere Lesezugriffe zulässt, spinlock_t slock = SPIN_LOCK_UNBLOCKED; unsigned long flags; spin_lock_irqsave( &slock, flags ); /* ktitischer Abschnitt */ spin_unlock_irq_restore( &slock, flags ); spinlock_t slock = SPIN_LOCK_UNLOCKED aber bei schreibendem Zugriff exklusiv ist (wie normaler Lock) // aktuelle Interrupts sichern // dann sperren // ursprüngl. Zustand restaurieren wenn zu Beginn alle Interrupts aktiviert sind, geht es auch einfacher: spinlock_t slock = SPIN_LOCK_UNBLOCKED; spin_lock_irq( &slock ); /* ktitischer Abschnitt */ spin_unlock_irq( &slock ); BS-I / Synchronisation / Praxis / Linux-Kernel auch Varianten für Interrupt-Behandlung: // aktuelle Interrupts sperren Cl. Schnörr / HM - read_lock_irq / read_unlock_irq - read_lock_irqsave / read_unlock_irqrestore 47 BS-I / Synchronisation / Praxis / Linux-Kernel - write_lock_irq / write_unlock_irq - write_lock_irw_save / write_unlock_irqrestore Cl. Schnörr / HM 48 Praxis / Linux-Kernel: Semaphore (1) Praxis / Linux-Kernel: Semaphore (2) Typ: semaphore Kernel-Semaphore sind „schlafende“ Locks statische Deklaration: static DECLARE_SEMAPHORE_GENERIC( name, count ); static DECLARE_MUTEX( name ); wartende Prozesse werden in Warteschlange gestellt, bei Freigabe wird erster geweckt eignen sich für Sperren, die über längeren Zeitraum gehalten werden (<-> Spin-Lock) // count = 1 dynamische Deklaration: sind nur im Prozess-Kontext, nicht in Interrupt-Handlern einsetzbar (Interrupt-Handler werden nicht vom Scheduler behandelt) sema_init( &sem, count ); init_MUTEX( &sem ); Code, der Semaphore verwendet, darf nicht bereits normalen Lock besitzen (Semaphore-Zugriff kann zum „schlafen-legen“ führen // count = 1; Verwendung mit up() und down(): down( &sem ); /* kritischer bereich */ up( &sem ); BS-I / Cl. Schnörr / HM Synchronisation / Praxis / Linux-Kernel 49 BS-I / Synchronisation / Praxis / Linux-Kernel Praxis / Linux-Kernel: Semaphore (3) 50 Cl. Schnörr / HM 52 Praxis: Synchronisation in C++ (1) Reader-Writer-Semaphore: analog zu Reader-Writer-Locks: Cl. Schnörr / HM Threads, Mutexe, usw.: Typ rw_semaphore, der spezielle Up- und DownOperationen für Lese- und Schreibzugriffe erlaubt pthreads-Bibliothek (UL-Threads): quasi-Standard in Unix/Linux wenig gebräuchlich unter Windows (<-> Win-API) alle RW-Semaphore sind Mutexe (bei Initialisierung count=1) Lesender Code: Schreibender Code: #include <pthreads.h>, linken mit libpthreads static DECLARE_RWSEM( rwsem ); init_rwsem( &rwsem ); down_read( &rwsem ); //read-only Abschnitt up_read( &rwsem ); C++0x/C++11-Standard: Thread/Mutex-API: down_write( &rwsem ); //lesen+schreiben-Abschnitt up_write( &rwsem ); #include <thread> OpenMP-Standard: Parallelisierung von Schleifen mittels Threads #include <omp.h> #pragma omp parallel for num_threads(NCPU) for ( long y = ylow; y <= yhigh; ++y ) {...} BS-I / Synchronisation / Praxis / Linux-Kernel Cl. Schnörr / HM 51 BS-I / Synchronisation / Praxis / C++ Praxis: Synchronisation in C++ (2) Praxis: Synchronisation in C++ (3) Nachbildung eines Monitors mittels pthreads struct ProducerConsumer { pthread_mutex_t count_mtx; //zu initialisieren pthread_cond_t full, empty; int count; ErzeugerVerbraucherprogramm void producer() { produce_item(); pc.enter(); } void remove() remove() { pthread_mutex_lock( pthread_mutex_lock( &count_mtx ); while ( count == 0 ) pthread_cond_wait( pthread_cond_wait( &empty, &count_mtx ); remove_item(); count--; if (count == N-1) pthread_cond_signal( pthread_cond_signal( &full ); pthread_mutex_unlock( pthread_mutex_unlock( &count_mtx ); } }; BS-I / Synchronisation / Praxis / C++ #pragma omp atomic newline statement_expression void consumer() { pc.remove(); consume_item(); } #pragma omp critical { statements; ... } Compiler-Unterstützung: Cl. Schnörr / HM 53 Kontextwechsel vor return --> Vor Rückgabe Veränderung von refcount durch andere Threads möglich refcount; //not ok! } Lösung: Mutex in Konstruktor und Destruktor class MutexObj { private: MutexObj( pthread_mutex_t & lock ) : _lock(lock) { pthread_mutex_lock( &_lock ); } ~MutexObj() { pthread_mutex_unlock( &_lock ); } pthread_mutex_t & / _lock; BS-I / Synchronisation Praxis / C++ }; __sync_lock_test_and_set( &lock, val ); InterlockedExchange( (long *)&lock, val ); Mutex-Objekt: return #include <atomic> OpenMP-Standard: Praxis: Synchronisation in C++ (4) int RefCount::dec_refcount() { pthread_mutex_lock( &lock ); --refcount; pthread_mutex_unlock( &lock ); C++0x/C++11-Standard: ProducerConsumer pc; Atomic-API: void enter() enter() { pthread_mutex_lock( pthread_mutex_lock( &count_mtx ); while ( count == N-1 ) pthread_cond_wait( pthread_cond_wait( &full, &count_mtx ); enter_item(); count++; if ( count == 0 ) pthread_cond_signal( pthread_cond_signal( &empty ); pthread_mutex_unlock( pthread_mutex_unlock( &count_mtx ); } RefCount::refcount; //in mehreren Objekten Atomare Befehle (--> Literatur): int RefCount::dec_refcount() { MutexObj lock( refcount_lock ); return --refcount; //ok } Synchronisation von refcount bis nach lokaler Kopie bei return Aber: nur ok bei return-per-value ! Cl. Schnörr / HM 55 BS-I / Synchronisation / Praxis / C++ // g++, testAndSet( _lock, 0 ); // msvc, , testAndSet( _lock, 0 ); Cl. Schnörr / HM 54