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.4 Das Modul threading  Zur nächsten ÜberschriftZur vorigen Überschrift

Mit dem Modul threading wird eine objektorientierte Schnittstelle für Threads angeboten.

Jeder Thread ist dabei eine Instanz einer Klasse, die von threading.Thread erbt. Da die Klasse selbst ein Teil des globalen Namensraums ist, eignen sich ihre statischen Member sehr gut, um Daten zwischen den Threads auszutauschen. Natürlich muss auch hier der Zugriff auf die von mehreren Threads genutzten Variablen durch Critical Sections gesichert werden.

Wir wollen ein Programm schreiben, das in mehreren Threads parallel prüfen kann, ob vom Benutzer eingegebene Zahlen Primzahlen [Eine Primzahl ist eine natürliche Zahl, die genau zwei Teiler besitzt. Die ersten sechs Primzahlen sind demnach 2, 3, 5, 7, 11 und 13. ] sind. Zu diesem Zweck definieren wir eine Klasse PrimzahlThread, die von threading.Thread erbt und als Parameter für den Konstruktor die zu überprüfende Zahl erwartet.

Die Klasse threading.Thread besitzt eine Methode namens start, die den Thread ausführt. Was genau ausgeführt werden soll, bestimmt die run-Methode, die wir mit unserer Primzahlberechnung überschreiben. Im ersten Schritt soll der Benutzer in einer Eingabeaufforderung Zahlen eingeben können, die dann überprüft werden. Ist die Überprüfung abgeschlossen, wird das Ergebnis auf dem Bildschirm ausgegeben. Das Programm inklusive der Klasse PrimzahlThread sieht dann folgendermaßen aus: [Der verwendete Algorithmus für die Primzahlprüfung ist sehr primitiv und dient hier nur als Beispiel für irgendeine rechenintensive Funktion. ]

import threading 
 
class PrimzahlThread(threading.Thread): 
    def __init__(self, zahl): 
        threading.Thread.__init__(self) 
        self.Zahl = zahl 
 
    def run(self): 
        i = 2 
        while i*i < self.Zahl: 
            if self.Zahl % i == 0: 
                print "%d ist nicht prim, da %d = %d * %d" % ( 
                    self.Zahl, self.Zahl, i, self.Zahl / i) 
                return 
            i += 1 
        print "%d ist prim" % self.Zahl 
 
meine_threads = [] 
 
while 1: 
    eingabe = raw_input("> ") 
    if eingabe == "ende": 
        break 
 
    thread = PrimzahlThread(long(eingabe)) 
    meine_threads.append(thread) 
    thread.start() 
 
for t in meine_threads: 
    t.join()

Innerhalb der Schleife wird die Eingabe vom Benutzer eingelesen, und es wird geprüft, ob es sich um das Schlüsselwort "ende" zum Beenden des Programms handelt. Wurde etwas anderes als "ende" eingegeben, wird eine neue Instanz der Klasse PrimzahlThread mit der Benutzereingabe als Parameter erzeugt und mit der start-Methode gestartet.

Das Programm verwaltet außerdem eine Liste namens meine_threads, in der alle Threads gespeichert werden. Nach dem Verlassen der Eingabeschleife wird über meine_threads iteriert und für jeden Thread die join-Methode aufgerufen. Mit join wird dafür gesorgt, dass das Hauptprogramm so lange wartet, bis alle gestarteten Threads beendet worden sind, denn join unterbricht die Programmausführung so lange, bis der Thread, für den es aufgerufen wurde, terminiert wurde.

Diese Methode, auf das Ende aller Threads zu warten, ist wesentlich eleganter als die im letzten Abschnitt verwendete Endlosschleife, da mit join keine Rechenzeit verschwendet und das Programm automatisch beendet wird, sobald kein Thread mehr läuft.

Ein Programmlauf könnte dann so aussehen, wobei die teils verzögerten Ausgaben zeigen, dass tatsächlich parallel gerechnet wurde:

> 737373737373737 
> 5672435793 
5672435793 ist nicht prim, da 5672435793 = 3 * 1890811931 
> 909091 
909091 ist prim 
> 10000000000037 
> 5643257 
5643257 ist nicht prim, da 5643257 = 23 * 245359 
> 4567 
4567 ist prim 
10000000000037 ist prim 
737373737373737 ist prim 
> ende

Galileo Computing - Zum Seitenanfang

18.4.1 Locking im threading-Modul  Zur nächsten ÜberschriftZur vorigen Überschrift

Genau wie das Modul thread bietet auch threading Methoden an, um den Zugriff auf Variablen abzusichern, die in mehreren Threads verwendet werden. Die dazu benutzten Lock-Objekte lassen sich dabei genauso wie die von thread.allocate_lock zurückgegebenen Objekte verwenden.

Um den Umgang mit Lock-Objekten zu zeigen, werden wir das Primzahlprogramm des letzten Abschnitts verbessern. Eine Schwachstelle des Programms bestand darin, dass, während der Benutzer gerade die nächste Zahl zur Prüfung eingibt, ein Thread im Hintergrund seine Arbeit beendet hat und sein Ergebnis auf den Bildschirm schreibt. Dadurch verliert der Benutzer unter Umständen die Übersicht, was er schon eingegeben hat, und es sieht äußerst unschön aus, wie das folgende Beispiel zeigt:

> 10000000000037 
> 5610000000000037 ist prim 
547 
56547 ist nicht prim, da 56547 = 3 * 18849 
> ende

In diesem Fall hat der Benutzer die Zahl 10000000000037 auf ihre Primzahleigenschaft hin untersuchen wollen. Unglücklicherweise wurde der Thread, der die Überprüfung übernahm, genau dann fertig, als der Benutzer bereits die ersten beiden Ziffern 56 der nächsten zu prüfenden Zahl, 56547, eingegeben hatte. Dies führte zu einer hässlichen »Zerstückelung« der Eingabe und sollte vermieden werden.

Wir werden zu diesem Zweck die Klasse PrimzahlThread mit einem statischen Attribut namens Ergebnis versehen, das in einem Dictionary die Ergebnisse der Berechnungen speichert. Dabei wird jeder zu prüfenden Zahl der Status bzw. das Ergebnis der Berechnung zugewiesen, wobei der Wert "in Arbeit" dafür steht, dass aktuell noch gerechnet wird, und der String "prim" anzeigt, dass es sich bei der Zahl um eine Primzahl handelt. Für Nicht-Primzahlen werden wir das gefundene Teilerprodukt in dem Dictionary speichern. Eine Momentaufnahme von PrimzahlThread.Ergebnis könnte dann folgendermaßen aussehen:

{ 
    737373737373737 : "in Arbeit", 
    5672435793 : "3 * 1890811931", 
    909091 : "prim", 
    10000000000037 : "in Arbeit", 
    5643257 :  "23 * 245359" 
}

In dem Beispiel befinden sich die Zahlen 737373737373737 und 10000000000037 noch in der Prüfung, während für 909091 bereits nachgewiesen werden konnte, dass sie eine Primzahl ist. 5672435793 und 5643257 sind keine Primzahlen, da sie sich über die angegebenen Produkte berechnen lassen.

In dem neuen Programm wird der Benutzer wie bisher Zahlen eingeben und das Programm durch die Eingabe von "ende" terminieren können. Zusätzlich wird es einen Befehl "status" geben, der den aktuellen Berechnungsstand, eben den Inhalt von PrimzahlThread.Ergebnis, ausgibt.

Da die Threads zum Setzen der jeweiligen Ergebnisse alle Primzahl Thread.Ergebnis verändern müssen, ist es notwendig, den Zugriff auf das Dictionary mittels einer Critical Section abzusichern. Das dazu erforderliche Lock-Objekt speichern wir in der statischen Variable PrimzahlThread.ErgebnisLock. Das neue Programm sieht damit wie folgt aus:

import threading 
 
class PrimzahlThread(threading.Thread): 
    Ergebnis = {} 
    ErgebnisLock = threading.Lock() 
 
    def __init__(self, zahl): 
        threading.Thread.__init__(self) 
        self.Zahl = zahl 
 
        PrimzahlThread.ErgebnisLock.acquire() 
        PrimzahlThread.Ergebnis[zahl] = "in Arbeit" 
        PrimzahlThread.ErgebnisLock.release() 
 
    def run(self): 
        i = 2 
        while i*i < self.Zahl + 1: 
            if self.Zahl % i == 0: 
                ergebnis = "%d * %d" % (i, self.Zahl / i) 
 
                PrimzahlThread.ErgebnisLock.acquire() 
                PrimzahlThread.Ergebnis[self.Zahl] = ergebnis 
                PrimzahlThread.ErgebnisLock.release() 
 
                return 
            i += 1 
 
        PrimzahlThread.ErgebnisLock.acquire() 
        PrimzahlThread.Ergebnis[self.Zahl] = "prim" 
        PrimzahlThread.ErgebnisLock.release() 
 
meine_threads = [] 
 
while 1: 
    eingabe = raw_input("> ") 
    if eingabe == "ende": 
        break 
 
    elif eingabe == "status": 
        print "-------- Aktueller Status --------" 
        PrimzahlThread.ErgebnisLock.acquire() 
        for z, e in PrimzahlThread.Ergebnis.iteritems(): 
            print "%d: %s" % (z, e) 
        PrimzahlThread.ErgebnisLock.release() 
        print "----------------------------------" 
 
    elif long(eingabe) not in PrimzahlThread.Ergebnis: 
        thread = PrimzahlThread(long(eingabe)) 
        meine_threads.append(thread) 
        thread.start() 
 
for t in meine_threads: 
    t.join()

Wie Sie sehen, sind alle schreibenden Zugriffe auf PrimzahlThread.Ergebnis durch die Aufrufe von acquire und release umgeben, wodurch das Dictionary gefahrlos in verschiedenen Threads verändert werden kann. Da sich ein Dictionary außerdem nicht verändern darf, während darüber iteriert wird, muss auch die Statusausgabe durch eine Critical Section gesichert werden.

In der Schleife für die Verarbeitung der Benutzerdaten ist neben der Ausgabe des aktuellen Status noch eine Abfrage hinzugekommen, die verhindert, dass dieselbe Zahl unnötigerweise mehr als einmal überprüft wird.

Ein Beispiellauf des Programms könnte dann so aussehen:

> 10000000000037 
> 5643257 
> 909091 
> 737373737373737 
> 56547 
> status 
-------- Aktueller Status -------- 
5643257: 5643257 * 245359 
909091: prim 
737373737373737: in Arbeit 
10000000000037: in Arbeit 
56547: 56547 * 18849 
---------------------------------- 
> status 
-------- Aktueller Status -------- 
5643257: 5643257 * 245359 
909091: prim 
737373737373737: in Arbeit 
10000000000037: prim 
56547: 56547 * 18849 
---------------------------------- 
> status 
--------- Aktueller Status -------- 
5643257: 5643257 * 245359 
909091: prim 
737373737373737: prim 
10000000000037: prim 
56547: 56547 * 18849 
---------------------------------- 
> ende

Mit dieser Version des Programms werden die angesprochenen Probleme zufriedenstellend beseitigt. Allerdings kann immer noch ein kleiner Schönheitsfehler auftreten: Wenn der Benutzer sehr viele, sehr große Zahlen eingibt, kann es passieren, dass das Programm eine lange Zeit rechnet, bevor das erste Ergebnis erzielt wird. Das rührt daher, dass sich die Threads gegenseitig ausbremsen, weil zwar alle Threads gleichzeitig ausgeführt werden, aber durch ihre große Anzahl für den einzelnen Thread nur wenig Rechenleistung übrig bleibt.

Um auch diese Unschönheit zu beseitigen, werden wir im nächsten Abschnitt eine Technik kennenlernen, mit der wir die Anzahl der Threads sinnvoll begrenzen können.


Galileo Computing - Zum Seitenanfang

18.4.2 Worker-Threads und Queues  Zur nächsten ÜberschriftZur vorigen Überschrift

In unseren bisherigen Programmen haben wir immer für jede Aufgabe einen neuen Thread gestartet, sodass es theoretisch beliebig viele Threads geben konnte. Wie am Ende des letzten Abschnitts angemerkt wurde, kann dies zu Geschwindigkeitsproblemen führen, wenn sehr viele Threads gleichzeitig laufen.

Dies lässt sich an einem Beispiel veranschaulichen: Wären wir ein Unternehmen, das für seine Kunden Zahlen daraufhin untersucht, ob sie Primzahlen sind, könnten wir uns unser Vorgehen so vorstellen, dass wir für jede Zahl, die wir überprüfen möchten, einen separaten Mathematiker einstellen, der mit den nötigen Berechnungen betraut wird. Hat der Mathematiker sein Werk vollendet, gibt er uns als Arbeitgeber Rückmeldung über das Ergebnis und wird entlassen.

In einem realen Unternehmen ist es nicht denkbar, für jede neue Aufgabe einen neuen Arbeiter einzustellen und diesen nach der Fertigstellung seiner Tätigkeit wieder zu entlassen. Vielmehr gibt es eine relativ konstante Anzahl von Arbeitern, denen die Aufgaben zugeteilt werden. Damit auch in diesem Modell eine beliebige Anzahl von Berechnungen durchgeführt werden kann, gibt es in unserer Firma einen Briefkasten, in den die Kunden die zu prüfenden Zahlen einwerfen können. Die Arbeiter holen sich dann selbstständig neue Aufgaben aus dem Briefkasten, sobald sie ihre vorherige Arbeit vollendet haben. Ist der Briefkasten einmal leer, warten die Arbeiter so lange, bis neue Zahlen eingeworfen werden.

In der Programmierung sprich man statt von Arbeitern von sogenannten Worker-Threads (von engl. to work = arbeiten). Der Briefkasten wird Queue (dt. Warteschlange) genannt.

Python hat ein eigenes Modul namens Queue, um mit Warteschlangen zu arbeiten. Der Konstruktor von Queue erwartet eine ganze Zahl als Parameter, die angibt, wie viele Elemente maximal in der Warteschlange stehen können. Ist der Parameter kleiner oder gleich 0, ist die Länge der Queue nicht begrenzt.

Queue-Instanzen verfügen im Wesentlichen über drei wichtige Methoden: put, get und task_done.

Mit der put-Methode können neue Aufträge in die Warteschlage eingestellt werden. Sie wird in unserem Beispiel vom Hauptprogramm benutzt werden, um neue Zahlen in den »Briefkasten« zu werfen.

Die Methode get liefert die nächste Aufgabe der Queue. Befindet sich gerade kein Arbeitsauftrag in der Warteschlange, blockiert get den Thread so lange, bis der nächste Auftrag verfügbar ist.

Hat ein Thread die Prüfung einer Zahl abgeschlossen, muss er dies der Queue mitteilen, indem er task_done aufruft. Die Warteschlange kümmert sich dabei selbstständig darum, dass das fertig verarbeitete Element entfernt wird.

Das folgende Beispiel wird fünf Worker-Threads einsetzen, die sich alle eine Queue teilen:

import threading 
import Queue 
 
class Mathematiker(threading.Thread): 
    Ergebnis = {} 
    ErgebnisLock = threading.Lock() 
 
    Briefkasten = Queue.Queue() 
 
    def run(self): 
        while True: 
            zahl = Mathematiker.Briefkasten.get() 
            ergebnis = self.istPrimzahl(zahl) 
 
            Mathematiker.ErgebnisLock.acquire() 
            Mathematiker.Ergebnis[zahl] = ergebnis 
            Mathematiker.ErgebnisLock.release() 
 
            Mathematiker.Briefkasten.task_done() 
 
    def istPrimzahl(self, zahl): 
        i = 2 
        while i*i < zahl + 1: 
            if zahl % i == 0: 
                return "%d * %d" % (zahl, zahl / i) 
 
            i += 1 
 
        return "prim" 
 
 
meine_threads = [Mathematiker() for i in range(5)] 
for thread in meine_threads: 
    thread.setDaemon(True) 
    thread.start() 
 
while True: 
    eingabe = raw_input("> ") 
    if eingabe == "ende": 
        break 
 
    elif eingabe == "status": 
        print "-------- Aktueller Status --------" 
        Mathematiker.ErgebnisLock.acquire() 
        for z, e in Mathematiker.Ergebnis.iteritems(): 
            print "%d: %s" % (z, e) 
        Mathematiker.ErgebnisLock.release() 
        print "----------------------------------" 
 
    elif long(eingabe) not in Mathematiker.Ergebnis: 
        Mathematiker.ErgebnisLock.acquire() 
        Mathematiker.Ergebnis[long(eingabe)] = "in Arbeit" 
        Mathematiker.ErgebnisLock.release()
Mathematiker.Briefkasten.put(long(eingabe)) Mathematiker.Briefkasten.join()

Die neben dem Einbau der Queue wichtigen Änderungen im Vergleich zum letzten Programm sind zum einen die run-Methode, die jetzt in einer Endlosschleife immer wieder neue Zahlen aus dem Briefkasten nimmt und mit der istPrimzahl-Methode überprüft, und zum anderen die Initialisierung und der Abschluss des Programms. Zu Anfang werden die fünf Worker-Threads in einer List Comprehension erzeugt und in der for-Schleife gestartet. Durch den Aufruf von thread.setDaemon(True) werden die Threads als sogenannte Dämon-Threads markiert. Der wesentliche Unterschied zwischen Dämon-Threads und normalen Threads besteht darin, dass ein Programm beendet wird, wenn nur noch Dämon-Threads laufen. Bei normalen Threads kann das Programm so lange laufen, bis auch der letzte Thread beendet worden ist.

Im Beispiel benötigen wir die Dämon-Threads deshalb, weil wir am Ende des Programms nicht wie bisher auf die Terminierung jedes Threads warten, sondern die join-Methode der Queue aufrufen. Mit dieser Methode join wird der Hauptprogramm-Thread so lange unterbrochen, bis alle noch in der Warteschlange stehenden Zahlen verarbeitet worden sind. Ist die Warteschlange leer, wird das Programm inklusive aller Worker-Threads beendet. Dass die Worker-Threads dabei nicht den Programmabbruch behindern können, wird durch setDaemon sichergestellt.

Falls Sie sich wundern, warum wir die Zugriffe auf die Queue nicht durch Critical Sections abgesichert haben, obwohl von allen Threads auf Mathematiker.Brief kasten zugegriffen wird, wundern Sie sich zu Recht: Normalerweise wäre es erforderlich, jedes Mal ein Lock-Objekt zu sperren und wieder zu entsperren. Allerdings nimmt uns das Queue-Modul von Python diese lästige Arbeit ab, wodurch die Arbeit mit Wartschlangen wesentlich komfortabler wird.

Wir werden uns jetzt noch zwei Klassen zuwenden, die für sehr spezielle Zwecke im Zusammenhang mit Threads dienen.


Galileo Computing - Zum Seitenanfang

18.4.3 Ereignisse definieren – threading.Event  Zur nächsten ÜberschriftZur vorigen Überschrift

Mit der Klasse threading.Event können sogenannte Ereignisse (engl. events) definiert werden, um Threads bis zum Eintritt eines bestimmten Ereignisses zu unterbrechen.

Ein Thread, der die wait-Methode eines frisch erzeugten threading.Event-Objekts aufruft, wird so lange unterbrochen, bis ein anderer Thread das Event mit set auslöst.

Ausführliche Informationen über threading.Event finden Sie in der Python-Dokumentation.


Galileo Computing - Zum Seitenanfang

18.4.4 Eine Funktion zeitlich versetzt ausführen – threading.Timer  topZur vorigen Überschrift

Das threading-Modul bietet eine praktische Klasse namens threading.Timer, um Funktionen nach dem Verstreichen einer gewissen Zeit aufzurufen.

threading.Timer(interval, function, args=[], kwargs={})

Der Parameter interval des Konstruktors gibt die Zeit in Sekunden an, die gewartet werden soll, bis die für function übergebene Funktion aufgerufen werden soll. Dabei können für interval sowohl Ganzzahlen aus auch float-Instanzen übergeben werden. Für args und kwargs kann eine Liste bzw. ein Dictionary übergeben werden, das die Parameter enthält, mit denen function aufgerufen werden soll.

Wir werden threading.Timer im nächsten Beispiel verwenden, um exemplarisch einen Wecker zu programmieren:

>>> import time, threading 
>>> def wecker(gestellt): 
        print "RIIIIIIIING!!!" 
        print "Der Wecker wurde um %s Uhr gestellt." % gestellt 
        print "Es ist nun %s Uhr" % time.strftime("%H:%M:%S") 
>>> timer = threading.Timer(30, wecker, 
                            [time.strftime("%H:%M:%S")]) 
>>> timer.start()

(30 Sekunden später)

>>> RIIIIIIIING!!! 
Der Wecker wurde um 03:11:26 Uhr gestellt. 
Es ist nun 03:11:58 Uhr

Mit der Methode start beginnt der Timer zu laufen und ruft dann – wie man der vorhergehenden Ausgabe entnehmen kann – nach der festgelegten Zeitspanne die übergebene Funktion auf. Die Differenz von 2 Sekunden rührt daher, dass zwischen dem Erstellen des Timer-Objekts und dem Aufrufen der start-Methode 2 Sekunden vergangen sind.

Nachdem die start-Methode aufgerufen wurde, kann der Timer außerdem mit der parameterlosen cancel-Methode wieder abgebrochen werden.



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