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

Inhaltsverzeichnis
Vorwort
Vorwort des Gutachters
1 Einstieg in C
2 Das erste Programm
3 Grundlagen
4 Formatierte Ein-/Ausgabe mit »scanf()« und »printf()«
5 Basisdatentypen
6 Operatoren
7 Typumwandlung
8 Kontrollstrukturen
9 Funktionen
10 Präprozessor-Direktiven
11 Arrays
12 Zeiger (Pointer)
13 Kommandozeilenargumente
14 Dynamische Speicherverwaltung
15 Strukturen
16 Ein-/Ausgabe-Funktionen
17 Attribute von Dateien und das Arbeiten mit Verzeichnissen (nicht ANSI C)
18 Arbeiten mit variabel langen Argumentlisten – <stdarg.h>
19 Zeitroutinen
20 Weitere Headerdateien und ihre Funktionen (ANSI C)
21 Dynamische Datenstrukturen
22 Algorithmen
23 CGI mit C
24 MySQL und C
25 Netzwerkprogrammierung und Cross–Plattform-Entwicklung
26 Paralleles Rechnen
27 Sicheres Programmieren
28 Wie geht’s jetzt weiter?
A Operatoren
B Die C-Standard-Bibliothek
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
C von A bis Z von Jürgen Wolf
Das umfassende Handbuch
Buch: C von A bis Z

C von A bis Z
3., aktualisierte und erweiterte Auflage, geb., mit CD und Referenzkarte
1.190 S., 39,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1411-7
Pfeil 26 Paralleles Rechnen
Pfeil 26.1 Parallelität
Pfeil 26.1.1 Single-Prozessorsysteme
Pfeil 26.1.2 Hyperthreading
Pfeil 26.2 Programmiertechniken der Parallelisierung
Pfeil 26.2.1 Automatische Parallelisierung
Pfeil 26.2.2 Halbautomatische Parallelisierung
Pfeil 26.2.3 Echte Parallelisierung
Pfeil 26.3 Vom Prozess zum Thread
Pfeil 26.4 Mit den POSIX-Threads programmieren
Pfeil 26.4.1 Ein serielles Beispiel
Pfeil 26.4.2 Das Grundgerüst für ein Programm mit mehreren Threads
Pfeil 26.4.3 Zusammenfassung


Rheinwerk Computing - Zum Seitenanfang

26.4 Mit den POSIX-Threads programmieren Zur nächsten ÜberschriftZur vorigen Überschrift

Um POSIX-Threads verwenden zu können, müssen Sie die Pthread-Bibliothek erst einmal installieren. Unter Linux lässt sie sich bequem mit dem jeweiligen Paketmanager nachinstallieren. Auch für Windows lässt sich die Pthread-Bibliothek ohne großen Aufwand nachinstallieren. Erste Anlaufstelle sollte hierbei die Webseite http://sourceware.org/pthreads-win32/ sein. Damit ich hier nicht mehrere Seiten für die Anleitung zur Installation und Verwendung der Pthread-Bibliothek verschwende, finden Sie auf der Buch-CD eine Beschreibung, wie Sie Anwendungen mit der Pthread-Bibliothek erstellen können. Dabei wird neben Linux auch auf die Verwendung von Pthreads mit Entwicklungsumgebungen wie Code::Blocks und Visual C++ von Microsoft eingegangen.


Rheinwerk Computing - Zum Seitenanfang

26.4.1 Ein serielles Beispiel Zur nächsten ÜberschriftZur vorigen Überschrift

Um Ihnen die Threads zu demonstrieren, habe ich ein absolut einfaches Beispiel verwendet. Wir verwenden zwei int-Arrays mit 100.000 Elementen mit absteigendem Wert – was bedeutet, dass alle Elemente der beiden Arrays sortiert werden müssen. Hierbei geht es mir nur darum, zwei CPU-rechenintensive Aufgaben zu beschleunigen. Als Sortieralgorithmus habe ich den etwas langsameren Bubblesort verwendet. Folgendes Beispiel soll anschließend parallel erstellt werden:

/* bubblesort.c */
#include <stdio.h>
#include <stdlib.h>
/* 100000 Elemente */
#define MAX 100000

/* ein Array von großen zu kleinen Werten */
int test_array1[MAX];
int test_array2[MAX];

/* in umgekehrter Reihenfolge erstellen */
void init_test_array(int *array) {
   int i, j;
   for(i = MAX,j=0; i >= 0; i--,j++)
      array[j] = i;
}

static void *bubble1(void* val) {
   int i, temp, elemente=MAX;
   while(elemente--)
      for(i = 1; i <= elemente; i++)
         if(test_array1[i-1] > test_array1[i]) {
            temp=test_array1[i];
            test_array1[i]=test_array1[i-1];
            test_array1[i-1]=temp;
         }
}

static void *bubble2(void* val) {
   int i, temp, elemente=MAX;
   while(elemente--)
      for(i = 1; i <= elemente; i++)
         if(test_array2[i-1] > test_array2[i]) {
            temp=test_array2[i];
            test_array2[i]=test_array2[i-1];
            test_array2[i-1]=temp;
         }
}

int main (void) {
   int i, j;
   init_test_array(test_array1);
   init_test_array(test_array2);

   bubble1(NULL);
   bubble2(NULL);

   /* Ausgabe in eine Textdatei */
   freopen("myoutput.txt", "w+", stdout);

   for(i = 0; i < MAX; i++)
      printf("[%d-%d]", test_array1[i], test_array2[i]);
   return EXIT_SUCCESS;
}

Die sortierten Elemente finden Sie anschließend in der Datei myoutput.txt, im selben Verzeichnis, in dem das Programm ausgeführt wird, wieder. Bei der Ausführung des Programms wird meine CPU (mit Dual-Core) ca. 50 % ausgelastet (siehe Abbildung 26.5). Neben der Anzeige der CPU-Auslastung wird auch noch der Verlauf beider CPUs bei der Auslastung angezeigt. Auch hieran kann man erkennen, dass nur eine CPU damit beschäftigt ist, das Array zu sortieren. Natürlich habe ich auch die einzelnen Prozesse überwacht, sodass mir nicht ein anderer Prozess mit einer rechenintensiven Anwendung dazwischenkommt.

Abbildung 26.5 »bubblesort.c« bei der Ausführung ohne Threads

Bei mehrfacher Ausführung des Programms lag die durchschnittliche Ausführzeit immer zwischen 25 und 28 Sekunden (was natürlich auch von der Rechenleistung abhängt).

Das Ziel dieses Beispiels soll es nun sein, mithilfe der POSIX-Thread-Bibliothek, die Sortierung der beiden Arrays parallel auszuführen – sprich, jede CPU soll hier die Sortierung eines Arrays durchführen. Hierdurch versprechen wir uns eine erheblich schnellere Ausführzeit des Programms.


Rheinwerk Computing - Zum Seitenanfang

26.4.2 Das Grundgerüst für ein Programm mit mehreren Threads Zur nächsten ÜberschriftZur vorigen Überschrift

Bevor Sie das Programm zum Sortieren der Arrays parallel machen werden, sollen hier die grundlegenden Funktionen von Pthreads etwas näher beschrieben werden, die dazu nötig sind.


Hinweis

Alle Funktionen der Pthread-Bibliothek geben bei Erfolg 0 und bei einem Fehler –1 zurück.


»pthread_create« – Einen neuen Thread erzeugen

Einen neuen Thread können Sie mit der Funktion pthread_create() erzeugen:

#include <pthread.h.>
int pthread_create( pthread_t *thread,
                    const pthread_attr_t *attribute,
                    void *(*funktion)(void *),
                    void *argumente );

Jeder Thread hat eine eigene Identifikationsnummer vom Datentyp pthread_t, die in der Adresse des ersten Parameters von pthread_create() abgelegt wird. Attribute für den Thread können Sie mit dem zweiten Parameter vergeben. Wird hierfür NULL angegeben, werden die Standard-Attribute verwendet. Auf die Attribute eines Threads wird gesondert eingegangen. Mit dem dritten Parameter geben Sie die »Funktion« für den Thread selbst an – hierbei handelt es sich um den eigentlichen neuen Thread. Hierzu muss die Anfangsadresse der Funktion angegeben werden. Die Argumente für den Thread vom dritten Parameter können Sie mit dem vierten Parameter übergeben. Gewöhnlich wird dieses Argument verwendet, um Daten an den Thread zu übergeben. In der Praxis handelt es sich hierbei meistens um eine Strukturvariable.

Einen Thread beenden

Um einen Thread zu beenden, gibt es im Grunde zwei Möglichkeiten: Entweder man verwendet funktionstypisch return oder die Funktion pthread_exit. In beiden Fällen muss der Rückgabewert vom Typ void* sein. Die Syntax zu pthread_exit() sieht so aus:

#include <pthread.h.>
void pthread_exit( void * wert );

Mit den beiden Möglichkeiten wird nur der jeweilige Thread beendet. Den Rückgabewert können Sie anschließend mit der Funktion pthread_join abfragen.


Hinweis

C-typisch darf der Rückgabewert eines Threads, wie schon bei den gewöhnlichen Funktionen, kein lokales Speicherobjekt sein, da auch hier der Speicher nach dem Beenden des Threads nicht mehr gültig ist.


Sollte allerdings irgendwo im Programm ein beliebiger Thread die Standard-Funktion exit() aufrufen, so bedeutet dies das Ende aller Threads einschließlich des Haupt-Threads.

Damit Sie sich nach der Beendigung eines Threads nicht noch um Reinigungsarbeiten wie das Freigeben der Ressourcen kümmern müssen, können Sie mit den Funktionen pthread_cleanup_push() und pthread_cleanup_pop() einen Exit-Handle einrichten. Ein solcher eingerichteter Handle wird dann immer ausgeführt, wenn ein Thread mit pthread_exit oder return beendet wurde. Im Grunde kann man diese Funktionen mit der Standardbibliotheksfunktion atexit() vergleichen. Anhand der Endung _push und _pop kann man schon erahnen, dass auch hier das Prinzip des Stacks verwendet wird. Hier die Syntax der beiden Funktionen:

#include <pthread.h.>
void pthread_cleanup_push( void (*function)(void *),
                           void *arg );
void pthread_cleanup_pop( int exec );

Mit pthread_cleanup_push() richten Sie den Exit-Handle ein. Als ersten Parameter geben Sie die Funktion an, die ausgeführt werden soll. Für die Argumente, die Sie der Funktion übergeben wollen, wird der zweite Parameter verwendet. Den zuletzt eingerichteten Exit-Handle können Sie wieder mit der Funktion pthread_cleanup_pop() vom Stack entfernen. Geben Sie allerdings einen Wert ungleich 0 als Parameter exec an, so wird diese Funktion zuvor noch ausgeführt, was bei einer Angabe von 0 nicht gemacht wird.


Hinweis

pthread_cleanup_push() und pthread_cleanup_pop() sind als Makros implementiert. Dabei ist pthread_cleanup_push() mit einer sich öffnenden und pthread_cleanup_pop() mit einer sich schließenden geschweiften Klammer implementiert. Das bedeutet: Sie müssen beide Funktionen im selben Anweisungsblock ausführen. Daher müssen Sie immer ein _push und ein _pop verwenden, auch wenn Sie wissen, dass eine _pop-Stelle nie erreicht wird.


»pthread_join« – Warten auf das Thread-Ende

Um aus dem Haupt-Thread auf das Ende und den Rückgabewert einzelner Threads zu warten, wird die Funktion pthread_join() verwendet:

#include <pthread.h.>
int pthread_join( pthread_t thread, void **thread_return );

pthread_join() hält den aufrufenden Thread (meistens den Haupt-Thread), der einen Thread mit pthread_create erzeugt hat, so lange an, bis der Thread thread vom Typ pthread_t beendet wurde. Der Exit-Status (bzw. Rückgabewert) des Threads wird an die Adresse von thread_return geschrieben. Sind Sie nicht am Rückgabewert interessiert, können Sie hier auch NULL verwenden.

Ein Thread, der sich beendet, wird eben so lange nicht »freigegeben« bzw. als beendeter Thread anerkannt, bis ein anderer Thread pthread_join aufruft. Daher sollte man für jeden erzeugten Thread einmal pthread_join aufrufen, es sei denn, man hat einen Thread mit pthread_detach »abgehängt«.

Ein paralleles Beispiel

Mit diesen wenigen Funktionen ist es nun möglich, eine echte parallele Anwendung zu erstellen. Hierzu folgt das Beispiel zum Sortieren der Arrays mit Bubblesort – mit dem Unterschied zum Beispiel zuvor, dass jetzt jede CPU ein Array zum Sortieren bekommt.

/* thread1.c */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
/* 100000 Elemente */
#define MAX 100000

/* ein Array von großen zu kleinen Werten */
int test_array1[MAX];
int test_array2[MAX];

/* in umgekehrter Reihenfolge erstellen */
void init_test_array(int *array) {
   int i, j;
   for(i = MAX,j=0; i >= 0; i--,j++)
      array[j] = i;
}

// Thread 1
static void *bubble1(void* val) {
   static int i, temp, elemente=MAX;
   printf("Thread bubble1() wurde gestartet\n");
   while(elemente--)
      for(i = 1; i <= elemente; i++)
         if(test_array1[i-1] > test_array1[i]) {
            temp=test_array1[i];
            test_array1[i]=test_array1[i-1];
            test_array1[i-1]=temp;
         }
   printf("Thread bubble1() wurde beendet\n");
   // Der Rückgabewert interessiert uns nicht.
   return NULL;
}

// Thread 2
static void *bubble2(void* val) {
   static int i, temp, elemente=MAX;
   printf("Thread bubble2() wurde gestartet\n");
   while(elemente--)
      for(i = 1; i <= elemente; i++)
         if(test_array2[i-1] > test_array2[i]) {
            temp=test_array2[i];
            test_array2[i]=test_array2[i-1];
            test_array2[i-1]=temp;
         }
   printf("Thread bubble2() wurde beendet\n");
   // Der Rückgabewert interessiert uns nicht.
   return NULL;
}


int main (void) {
    pthread_t thread1, thread2;
    int i, rc;

    // Ausgabe in eine Textdatei
    freopen("myoutput.txt", "w+", stdout);

    printf("Haupt-Thread main() wurde gestartet\n");
    // beide Arrays mit Werten initialisieren
    init_test_array(test_array1);
    init_test_array(test_array2);
    // Thread 1 erzeugen
    rc = pthread_create( &thread1, NULL, &bubble1, NULL );
    if( rc != 0 ) {
        printf("Konnte Thread 1 nicht erzeugen\n");
        return EXIT_FAILURE;
    }
    // Thread 2 erzeugen
    rc = pthread_create( &thread2, NULL, &bubble2, NULL );
    if( rc != 0 ) {
        printf("Konnte Thread 2 nicht erzeugen\n");
        return EXIT_FAILURE;
    }
    // Main-Thread wartet auf beide Threads.
    pthread_join( thread1, NULL );
    pthread_join( thread2, NULL );

    // das Ergebnis der Sortierung in die Datei
    // myoutput.txt schreiben
    for(i = 0; i < MAX; i++) {
       printf("[%d-%d]", test_array1[i], test_array2[i]);
    }
    printf("\nHaupt-Thread main() wurde beendet\n");
    return EXIT_SUCCESS;
}

Die Ausführung des Programms an sich entspricht dem seriellen Beispiel. Die Arrays werden hier ebenfalls sortiert, und das Ergebnis wird in die Datei myoutput.txt geschrieben. Uns interessiert hierbei allerdings eher die Auslastung der CPUs und natürlich die Zeit, die diese Sortierung mit der parallelen Version benötigt. Ein Blick auf die Auslastung der CPU zeigt jetzt das gewünschte Ergebnis. Die beiden CPUs sind beide parallel zu 100 % ausgelastet und verrichten auch gleichzeitig ihre Arbeit.

Abbildung 26.6 Bubblesort bei der parallelen Ausführung

Auch die Ausführzeit hat sich gewaltig verändert. Anstatt der bisher 25–28 Sekunden verrichtet unser Programm jetzt seine Arbeit in 11–14 Sekunden. Wir haben damit die Ausführzeit quasi halbiert.


Hinweis

Das Beispiel ließe sich sicherlich noch mehr optimieren. Es wurde nämlich noch nicht die Cacheline-Größe des Prozessors beachtet, die üblicherweise 128 Byte beträgt. Hierbei kann es beispielsweise passieren, dass Variablen von zwei verschiedenen Threads in einer Cacheline liegen. Verändert hierbei ein Thread den Wert seiner Variablen, wird das Invalid-Bit für den anderen Prozessor gesetzt. Dadurch muss der andere Prozessor den Wert erneut in den Cache laden. Dies kann die Performance der Anwendung erheblich bremsen.



Rheinwerk Computing - Zum Seitenanfang

26.4.3 Zusammenfassung topZur vorigen Überschrift

Anhand dieser kurzen Einführung zu den POSIX-Threads lässt sich schon erkennen, wie brisant und aktuell das Thema ist. Dennoch soll gesagt werden, dass Threads wirklich nur da eingesetzt werden sollten, wo sie unbedingt benötigt werden. Lassen Sie sich von diesem Kapitel nicht täuschen: Threads sind nicht immer so einfach zu implementieren. Häufig sind Synchronisationsmechanismen zu implementieren, wenn mehrere Threads die Daten teilen. Weiß man hier nicht genau, was man tut, kann es passieren, dass die Threads Amok laufen oder dass es Datensalat gibt. Es gibt Probleme, die lassen sich sehr gut und einfach parallelisieren. Andere wiederum benötigen einfache Synchronisationsmechanismen, und wieder andere brauchen einen gewaltigen Verwaltungsaufwand, sodass sich ein Parallelisieren fast nicht lohnt.


Hinweis

Mehr zu den POSIX-Threads finden Sie auf der Buch-CD in einem Kapitel aus dem Buch »Linux-UNIX-Programmierung« vom selben Verlag (und aus meiner Feder ;-)). Die Beispiele lassen sich selbstverständlich auch unter Windows ausführen und verwenden, sofern Sie die Phtread-Bibliothek installiert haben. Wie dies geht, ist ebenfalls auf der Buch-CD beschrieben.




Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  
  Zum Katalog
Zum Katalog: C von A bis Z

 C von A bis Z
Jetzt bestellen


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

 Buchtipps
Zum Katalog: C/C++






 C/C++


Zum Katalog: Einstieg in C






 Einstieg in C


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: IT-Handbuch für Fachinformatiker






 IT-Handbuch für
 Fachinformatiker


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2009
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.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de