Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
1 Einleitung
2 Überblick über Python
3 Die Arbeit mit Python
4 Der interaktive Modus
5 Grundlegendes zu Python-Programmen
6 Kontrollstrukturen
7 Das Laufzeitmodell
8 Basisdatentypen
9 Benutzerinteraktion und Dateizugriff
10 Funktionen
11 Modularisierung
12 Objektorientierung
13 Weitere Spracheigenschaften
14 Mathematik
15 Strings
16 Datum und Zeit
17 Schnittstelle zum Betriebssystem
18 Parallele Programmierung
19 Datenspeicherung
20 Netzwerkkommunikation
21 Debugging
22 Distribution von Python-Projekten
23 Optimierung
24 Grafische Benutzeroberflächen
25 Python als serverseitige Programmiersprache im WWW mit Django
26 Anbindung an andere Programmiersprachen
27 Insiderwissen
28 Zukunft von Python
A Anhang
Stichwort

Download:
- ZIP, ca. 4,8 MB
Buch bestellen
Ihre Meinung?

Spacer
 <<   zurück
Python von Peter Kaiser, Johannes Ernesti
Das umfassende Handbuch - Aktuell zu Python 2.5
Buch: Python

Python
gebunden, mit CD
819 S., 39,90 Euro
Galileo Computing
ISBN 978-3-8362-1110-9
Pfeil 18 Parallele Programmierung
  Pfeil 18.1 Prozesse, Multitasking und Threads
  Pfeil 18.2 Die Thread-Unterstützung in Python
  Pfeil 18.3 Das Modul thread
    Pfeil 18.3.1 Datenaustausch zwischen Threads – locking
  Pfeil 18.4 Das Modul threading
    Pfeil 18.4.1 Locking im threading-Modul
    Pfeil 18.4.2 Worker-Threads und Queues
    Pfeil 18.4.3 Ereignisse definieren – threading.Event
    Pfeil 18.4.4 Eine Funktion zeitlich versetzt ausführen – threading.Timer


Galileo Computing - Zum Seitenanfang

18.3 Das Modul thread  Zur nächsten ÜberschriftZur vorigen Überschrift

Das Modul thread kann einzelne Funktionen in einem separaten Thread ausführen. Dazu dient die Funktion thread.start_new_thread, die mindestens zwei Parameter erwartet:

thread.start_new_thread(function, args[, kwargs])

Der Parameter function muss dabei eine Referenz auf die Funktion enthalten, die ausgeführt werden soll. Mit args muss eine tuple-Instanz übergeben werden, die die Parameter für function enthält.

Mit dem optionalen Parameter kwargs kann ein Dictionary übergeben werden, das zusätzliche Schlüsselwortparameter für die Funktion function bereitstellt.

Als Rückgabewert gibt thread.start_new_thread eine Zahl zurück, die den erzeugten Thread eindeutig identifiziert.

Nachdem function verlassen wurde, wird der Thread automatisch gelöscht.

Parallele Berechnung von Pi

Als Beispiel für das Multithreading werden wir eine Funktion entwickeln, mit der die Kreiszahl π mithilfe des Wallis’schen Produkts berechnet werden kann, das der englische Mathematiker John Wallis (1616 – 1703) im Jahre 1655 entdeckte:

Im Zähler stehen dabei immer gerade Zahlen, die sich bei jedem zweiten Faktor um 2 erhöhen. Der Nenner enthält nur ungerade Zahlen, die sich mit Ausnahme des ersten Faktors ebenfalls alle zwei Faktoren um 2 erhöhen.

Die Funktion naehere_pi_an, die als Parameter die Anzahl der zu berücksichtigenden Faktoren erhält, kann damit folgendermaßen definiert werden:

def naehere_pi_an(n): 
    pi_halbe = 1 
    zaehler, nenner = 2.0, 1.0 
 
    for i in xrange(n): 
        pi_halbe *= zaehler / nenner 
        if i % 2: 
            zaehler += 2 
        else: 
            nenner += 2 
 
    print "Annaeherung mit %d Faktoren: %.16f" % (n, 2*pi_halbe)

Wenn für n der Wert 1000 übergeben wird, erzeugt die Funktion folgende Ausgabe, bei der nur die ersten beiden Nachkommastellen korrekt sind:

>>> naehere_pi_an(1000) 
Annaeherung mit 1000 Faktoren: 3.140023818600586200

Wirklich brauchbare Näherungen werden erst für recht große n erzielt, was aber auch mit wesentlich mehr Rechenzeit bezahlt werden muss. Beispielsweise benötigte ein Aufruf mit n = 10000000 auf unserem Testrechner ca. sieben Sekunden.

Im nächsten Programm werden wir mithilfe von thread.start_new_thread mehrere Threads erzeugen, die die Funktion naehere_pi_an für verschiedene n aufrufen.

import thread 
 
thread.start_new_thread(naehere_pi_an, (10000000,)) 
thread.start_new_thread(naehere_pi_an, (10000,)) 
thread.start_new_thread(naehere_pi_an, (99999999,)) 
thread.start_new_thread(naehere_pi_an, (123456789,)) 
thread.start_new_thread(naehere_pi_an, (,), {"n" : 1337}) 
 
while True: 
    pass

Die Endlosschleife am Ende des Programms ist deshalb notwendig, damit der Thread des Hauptprogramms auf die anderen Threads wartet und nicht sofort beendet wird. Es ist nämlich so, dass alle Threads eines Programms sofort abgebrochen werden, wenn das Hauptprogramm sein Ende erreicht hat.

Eine Endlosschleife für diesen Zweck zu benutzen ist natürlich sehr unschön, weil sie Rechenleistung sinnlos vergeudet und das Programm mit + beendet werden muss. Wir werden erst bei dem Modul threading bessere Methoden kennenlernen, um einen Thread auf das Ende eines anderen warten zu lassen.

Das Interessante an diesem Programm ist die Reihenfolge der Ausgabe, die nicht mit der Reihenfolge der Aufrufe übereinstimmt:

Annaeherung mit 1337 Faktoren: 3.1427668611489281 
Annaeherung mit 10000 Faktoren: 3.1414355935898644 
Annaeherung mit 100000 Faktoren: 3.1415769458226377 
Annaeherung mit 1234569 Faktoren: 3.1415939259321926 
Annaeherung mit 11111111 Faktoren: 3.1415927949601699

Je größer das übergebene n war, desto länger musste auf die Ausgabe der dazugehörigen Annäherung von π gewartet werden, ganz egal, wann die Funktion gestartet wurde. Offensichtlich liefen alle Berechnungen parallel ab, wie wir es erwartet hatten.

Im letzten Beispiel hatte jeder Thread seine eigenen Variablen und musste keine Daten mit anderen Threads austauschen. Im nächsten Abschnitt werden wir uns mit dem Datenaustausch zwischen Threads beschäftigen.


Galileo Computing - Zum Seitenanfang

18.3.1 Datenaustausch zwischen Threads – locking  topZur vorigen Überschrift

Threads haben gegenüber Prozessen den Vorteil, dass sie sich dieselben globalen Variablen teilen und deshalb sehr einfach Daten austauschen können. Trotzdem gibt es ein paar Stolperfallen, die beim Zugriff auf dieselbe Variable durch mehrere Threads beachtet werden müssen.

Würde man beispielsweise unser vorhergehendes Beispiel um einen Zähler erweitern, der die Anzahl der zurzeit aktiven Threads enthält, damit das Programm nach dem Beenden aller Berechnungen von selbst terminiert, könnte man ganz naiv folgende Implementation vorschlagen:

import thread 
 
anzahl_threads = 0 
 
def naehere_pi_an(n): 
    global anzahl_threads 
    anzahl_threads += 1 
 
    # hier wurde der Berechnungscode zur Übersicht ausgelassen 
 
    anzahl_threads -= 1 
 
thread.start_new_thread(naehere_pi_an, (10000000,)) 
thread.start_new_thread(naehere_pi_an, (10000,)) 
thread.start_new_thread(naehere_pi_an, (99999999,)) 
thread.start_new_thread(naehere_pi_an, (123456789,)) 
thread.start_new_thread(naehere_pi_an, (), {"n" : 1337}) 
 
while anzahl_threads > 0: 
    pass

Dieses Programm hat zwei schwerwiegende Fehler. Erstens funktioniert es nicht, weil die while-Schleife erreicht wird, bevor überhaupt ein Thread gestartet werden konnte. Dies liegt einfach daran, dass die Zeitscheibe des Hauptprogramms nach den Aufrufen von thread.start_new_thread noch nicht aufgebraucht war und deshalb die Schleife zu laufen beginnt, bevor auch nur ein einziger Thread seine Arbeit aufgenommen hat.

Aber selbst, wenn dieses Problem bereits gelöst wäre, kann sich das Programm unter Umständen fehlerhaft verhalten. Die Gefahr lauert in den beiden Zeilen, die den Wert der globalen Variable anzahl_threads verändern:

Es ist theoretisch möglich, dass das Zeitfenster eines Threads genau während der Veränderung von anzahl_threads endet, denn Zuweisungen bestehen intern aus mehreren Schritten. Zuerst muss der Wert von anzahl_threads gelesen werden, dann muss eine neue Instanz mit dem um eins vergrößerten bzw. verringerten Wert erzeugt werden, die im letzten Schritt mit der Referenz anzahl_threads verknüpft wird.

Wenn ein Thread A nun beim Erhöhen von anzahl_threads während der Erzeugung der neuen Instanz schlafen gelegt wird, könnte ein anderer Thread B aktiviert werden, der ebenfalls anzahl_threads erhöhen möchte. Weil aber der Thread A seinen neuen Wert von anzahl_threads noch nicht berechnet und auch nicht mit der Referenz verknüpft hat, würde der neu aktivierte Thread B den alten Wert von anzahl_threads lesen und erhöhen. Wird dann später der Thread A wieder aktiv, erhöht er den schon vorher eingelesenen Wert um eins und weist ihn anzahl_threads zu. Das Ende vom Lied wäre ein um eins zu kleiner Wert von anzahl_threads, wodurch die Schleife im Hauptprogramm endlos laufen würde.

Die folgende Tabelle soll das beschriebene Szenario veranschaulichen:


Tabelle 18.1  Problemszenario beim gleichzeitigen Zugriff auf eine globale Variable
Zeitfenster Thread A Thread B

1

Wert von anzahl_threads einlesen. Beispielsweise 2.

schläft

--------- Zeitfenster von A endet, und der Thread B wird aktiviert. -------------

2

schläft

Wert von anzahl_threads einlesen. In diesem Fall 2.

Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 3.

Die neue Instanz an die Referenz anzahl_threads knüpfen. Damit verweist anzahl_threads auf den Wert 3.

--------- Zeitfenster von B endet, und der Thread A wird aktiviert. -------------

3

Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 3.

Die neue Instanz an die Referenz anzahl_threads knüpfen. Damit verweist anzahl_threads auf den Wert 3.

schläft


Im Beispiel wurde anzahl_threads also nur um 1 erhöht, obwohl zwei neue Threads gestartet wurden.

Um solche Probleme zu vermeiden, kann ein Programm Stellen markieren, die nicht parallel in mehreren Threads laufen dürfen. Man bezeichnet solche Stellen auch als Critical Sections (dt. kritische Abschnitte).

Critical Sections werden durch sogenannte Lock-Objekte (von engl. to lock = sperren) realisiert. Mithilfe der parameterlosen Funktion thread.allocate_lock kann ein neues Lock-Objekt erzeugt werden:

lock_objekt = thread.allocate_lock()

Lock-Objekte haben die beiden wichtigen Methoden acquire und release, die jeweils beim Betreten bzw. beim Verlassen einer Critical Section aufgerufen werden müssen. Wenn die acquire-Methode eines Lock-Objekts aufgerufen wurde, ist es gesperrt. Ruft ein Thread die acquire-Methode eines gesperrten Lock-Objekts auf, muss er so lange warten, bis das Lock-Objekts wieder mit release freigegeben worden ist. Durch diese Technik wird verhindert, dass eine Critical Section von mehreren Threads gleichzeitig ausgeführt werden kann.

Wir können unser Beispielprogramm folgendermaßen um Critical Sections erweitern, wobei wir außerdem einen Schalter namens thread_gestartet einfügen, damit das Hauptprogramm mindestens so lange wartet, bis die Threads gestartet worden sind. Der Zugriff auf die Variablen anzahl_threads und thread_gestartet wird durch das Lock-Objekt lock gesichert:

import thread 
 
anzahl_threads = 0 
thread_gestartet = False 
 
lock = thread.allocate_lock() 
 
def naehere_pi_an(n): 
    global anzahl_threads, thread_gestartet 
 
    lock.acquire() 
    anzahl_threads += 1 
    thread_gestartet = True 
    lock.release() 
 
    # hier wurde der Berechnungscode zur Übersicht ausgelassen 
 
    lock.acquire() 
    anzahl_threads -= 1 
    lock.release() 
 
thread.start_new_thread(naehere_pi_an, (100000,)) 
thread.start_new_thread(naehere_pi_an, (10000,)) 
thread.start_new_thread(naehere_pi_an, (11111111,)) 
thread.start_new_thread(naehere_pi_an, (1234569,)) 
thread.start_new_thread(naehere_pi_an, (), {"n" : 1337}) 
 
while not thread_gestartet: 
    pass 
 
while anzahl_threads > 0: 
    pass

Am Anfang des Programms wird der Schalter thread_gestartet auf False gesetzt, und mittels thread.allocate_lock() wird ein neues Lock-Objekt erzeugt. Innerhalb von naehere_pi_an gibt es dann eine Critical Section, in der anzahl_threads an die Anzahl der laufenden Threads angepasst bzw. die Variable thread_gestartet auf True gesetzt wird.

Die erste while-Schleife des Hauptprogramms sorgt nun dafür, dass auf jeden Fall so lange gewartet wird, bis ein Thread gestartet worden ist und den Wert von thread_gestartet auf True gesetzt hat. Die zweite Schleife sorgt wie gehabt dafür, dass das Programm so lange läuft, wie noch Threads ausgeführt werden.

Um die Wirkungsweise eines Lock-Objekts zu verdeutlichen, zeigt Ihnen die folgende Tabelle, wie unser Problemszenario durch die Critical Sections gelöst wird:


Tabelle 18.2  Lösung des »anzahl_threads«-Problems mit einem Lock-Objekt
Zeitfenster Thread A Thread B

1

Das Lock-Objekt mit lock.acquire() sperren.

Wert von anzahl_threads einlesen. Beispielsweise 2.

schläft

--------- Zeitfenster von A endet, und der Thread B wird aktiviert. -------------

2

schläft

lock.acquire wird aufgerufen, aber das Lock-Objekt ist bereits gesperrt. Deshalb wird B schlafen gelegt.

--- B wurde durch lock.acquire schlafen gelegt. A wird weiter ausgeführt. ----

3

Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 3.

Die neue Instanz an die Referenz anzahl_threads knüpfen. Damit verweist anzahl_threads auf den Wert 3.

Das Lock-Objekt wird mittels lock.release() wieder freigegeben.

schläft

--------- Zeitfenster von A endet, und der Thread B wird aktiviert. -------------

4

schläft

Das Lock-Objekt wird automatisch gesperrt, da B lock.acquire aufgerufen hat.

Wert von anzahl_threads einlesen. In diesem Fall 3.

Den Wert um 1 erhöhen. Im Speicher existiert nun eine neue Instanz mit dem Wert 4.

Die neue Instanz an die Referenz anzahl_threads knüpfen. Damit verweist anzahl_threads auf den Wert 4.

Das Lock-Objekt wird mit lock. release() wieder freigegeben.


Sie sollten darauf achten, dass Sie in Ihren eigenen Programmen alle Stellen, in denen Probleme durch Zugriffe von mehreren Threads vorkommen können, durch Critical Sections schützen.

Unzureichend abgesicherte Programme mit mehreren Threads können sehr schwer reproduzierbare und lokalisierbare Fehler produzieren. Die Herausforderung beim Umgang mit Threads besteht deshalb darin, solche Probleme zu umgehen.


Achtung
Wenn Sie mehrere Lock-Objekte verwenden, kann es passieren, dass sich ein Programm in einem sogenannten Deadlock aufhängt, weil zwei gelockte Threads gegenseitig aufeinander warten.




Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.






 <<   zurück
  
  Zum Katalog
Zum Katalog: Python






Python
bestellen
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Linux






 Linux


Zum Katalog: Ubuntu GNU/Linux






 Ubuntu GNU/Linux


Zum Katalog: Praxisbuch Web 2.0






 Praxisbuch Web 2.0


Zum Katalog: UML 2.0






 UML 2.0


Zum Katalog: Praxisbuch Objektorientierung






 Praxisbuch Objektorientierung


Zum Katalog: Einstieg in SQL






 Einstieg in SQL


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für Fachinformatiker


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de