Die Programmiersprache Ruby (German Edition)
server.accept # Darauf warten, dass sich ein Client verbindet
Thread.start(client) do |c| # Einen neuen Thread starten
handle_client(c) # Den Client durch diesen Thread betreuen
end
end
9.9.6.3 Parallele Iteratoren
Auch wenn Ein-/Ausgabeaufgaben der typische Anwendungsfall für Threads in Ruby sind, gibt es noch andere Bereiche, in denen sie nutzbar sind. Der folgende Code fügt dem Modul
Enumerable
eine Methode
conmap
hinzu. Sie funktioniert wie
map
, verarbeitet aber jedes Element des Eingabe-Array als eigenen Thread:
module Enumerable # Öffne das Modul Enumerable
def conmap(&block) # Definiere eine neue Methode, die einen Block erwartet
threads = [] # Beginne mit einem leeren Thread-Array
self.each do |item| # Für jedes enumerierbare Element
# Rufe den Block in einem neuen Thread auf und merke dir den Thread.
threads << Thread.new { block.call(item) }
end
# Jetzt bilde das Array mit Threads auf ihre Werte ab.
threads.map {|t| t.value } # und gibt das Array mit diesen Werten zurück
end
end
Hier eine ähnliche Parallelversion für den Iterator
each
:
module Enumerable
def concurrently
map {|item| Thread.new { yield item }}.each {|t| t.join }
end
end
Dieser Code ist knapp und knackig: Wenn Sie ihn verstehen können, beherrschen Sie die Ruby-Syntax und Ruby-Iteratoren schon sehr gut.
Denken Sie daran, dass Standarditeratoren, denen kein Block übergeben wurde, in Ruby 1.9 ein Enumerator-Objekt zurückgeben. Das bedeutet, dass wir mit der oben definierten Methode
concurrently
und einem
Hash
-Objekt
h
schreiben können:
h.each_pair.concurrently {|*pair| process(pair)}
9.9.7 Thread Exclusion und Deadlock
Wenn zwei Threads auf dieselben Daten zugreifen und mindestens einer der Threads diese Daten verändert, müssen Sie besonders darauf achten, dass kein Thread die Daten in einem inkonsistenten Zustand zu sehen bekommt. Dies wird als Thread Exclusion bezeichnet. Ein paar Beispiele werden erläutern, warum es notwendig ist.
Stellen Sie sich als Erstes vor, dass zwei Threads Dateien verarbeiten und jeder Thread eine gemeinsam genutzte Variable erhöht, um die Anzahl der schon verarbeiteten Dateien zu verfolgen. Das Problem ist, dass das Erhöhen einer Variable keine atomare Operation ist. Das bedeutet, dass es nicht in einem einzelnen Schritt geschieht: Um eine Variable zu erhöhen, muss ein Ruby-Programm den Wert lesen, eine
1
hinzufügen und dann den neuen Wert wieder in die Variable schreiben. Stellen Sie sich vor, dass unser Zähler bei
100
steht und nun zwei Threads versetzt ausgeführt werden. Der erste Thread liest den Wert
100
, aber bevor er eine
1
hinzufügen kann, stoppt der Scheduler die Ausführung des ersten Threads und erlaubt dem zweiten, loszulaufen. Nun liest der zweite Thread den Wert
100
, fügt
1
hinzu und speichert die
101
wieder in der Zählervariablen. Dieser zweite Thread liest nun eine neue Datei ein, wodurch er geblockt wird, so dass der erste Thread weiterlaufen kann. Der erste Thread fügt nun der
100
eine
1
hinzu und speichert das Ergebnis. Beide Threads haben den Zähler erhöht, aber er ist nun bei
101
, nicht bei
102
.
Ein anderes traditionelles Beispiel für die Notwendigkeit der Thread Exclusion dreht sich um eine Anwendung für Banktransaktionen. Stellen Sie sich vor, dass ein Thread den Transfer von Geld von einem Sparkonto auf ein Girokonto durchführt, während ein anderer Thread monatliche Kontoauszüge erzeugt, die an den Kunden geschickt werden. Ohne eine korrekte Exclusion kann der Thread zum Erzeugen der Auszüge die Kontodaten des Kunden lesen, wenn das Geld vom Sparbuch abgezogen, aber noch nicht auf dem Girokonto eingetragen wurde.
Wir lösen solche Probleme durch die Anwendung eines kooperativen Sperrmechanismus. Jeder Thread, der auf gemeinsam genutzte Daten zugreifen will, muss diese Daten zuerst sperren . Die Sperre wird von einem
Mutex
-Objekt repräsentiert (Kurzform von »Mutual Exclusion«). Um ein
Mutex
zu sperren, rufen Sie seine Methode
lock
auf. Wenn Sie die gemeinsam genutzten Daten fertig gelesen oder geändert haben, rufen Sie die Methode
unlock
des
Mutex
auf. Die Methode
lock
blockiert, wenn sie für ein schon gesperrtes
Mutex
aufgerufen wird, und kehrt erst dann zurück, wenn der Aufrufende erfolgreich eine Sperre für sich erhalten hat. Wenn jeder Thread, der auf die gemeinsamen Daten zugreift, das
Mutex
korrekt sperrt und entsperrt, wird kein Thread die Daten in einem inkonsistenten Zustand zu Gesicht bekommen, und wir werden
Weitere Kostenlose Bücher