Parallelisierung mit der POSIX® Thread Bibliothek (article in german)

 

Dieser Artikel ist erschienen im entwickler magazin - Heft 6.07  - Abdruck mit freundlicher Genehmigung der Zeitschrift: http://derentwickler.de

 

 

Parallelisierung mit der POSIX® Thread Bibliothek

In diesem Artikel wird der Ansatz zur Parallelisierung von sequentiellen Programmen mit Hilfe einer Threading-Bibliothek vorgestellt. Der Schwerpunkt liegt auf der Darstellung der Konzepte am Beispiel der plattformunabhängigen POSIX® Thread Bibliothek (Pthreads) und nicht auf einer vollständigen Beschreibung. Das Ziel dieses Artikels ist es, in einfachen Beispielen in die wesentlichen Grundlagen der Thread-Programmierung einzuführen und auf Chancen und Probleme vertiefend einzugehen.

Dr. Mario Deilmann, Intel GmbH

Einführung

 

Wie ich bereits ausführlich in den beiden anderen Artikeln zur Multicore-Programmierung vorgestellt habe , gehören die Ansätze zur Parallelisierung auf Basis von Threading Bibliotheken zu den Shared-Memory-Modellen. Die wichtigsten Punkte dieses Ansatzes werden an dieser Stelle kurz wiederholt: Der Entwickler erzeugt mehrere nebenläufige Programmfäden (Threads) von Hand, um die zu parallelisierenden Aufgaben gleichzeitig zu verteilen. Jeder Thread besteht dabei aus einem seriellen Strom von Instruktionen. Die verschiedenen Threads teilen sich mit ihrem Prozess eine Reihe von Betriebsmitteln, wie das Codesegment für die Instruktionen, das Datensegment für die globalen und dynamischen Variablen sowie die Dateideskriptoren. Die Threads kommunizieren über den gemeinsamen Adressraum. Jeder Thread besitzt einen eigenen Befehlszähler, einen Stack, für die lokalen Variablen und es gibt die Möglichkeit, einen eigenen lokalen Speicherbereich anzulegen (engl. Thread-local Storage). Abbildung eins zeigt die Unterschiede zum sequentiellen Ansatz. Außerdem besitzt jeder Thread einen Zustand, der inaktiv, bereit, rechnend oder blockiert sein kann. Da es durch die gemeinsame Nutzung der Systemressourcen ( z.B. Speicher, I/O) zu Konflikten kommen kann, müssen diese durch den Einsatz der verschiedenen Synchronisationsmechanismen aufgelöst werden. Die Reihenfolge, Priorität, sowie das Umschalten der Threads, ist abhängig von der Implementierung des Betriebssystem-Steuerprogramms (engl. scheduler).

 bild1.jpg

Abb. 1: Der Adressraum bei seriellen und parallelen Programmen

 

Das Portable Operating System Interface (POSIX®) ist eine Programmierschnittstelle zur Anwendungsprogrammierung der Open Group für Unix und IEEE. Bereits 1995 wurden die Funktionen zur Arbeit mit Threads (API) standardisiert (IEEE POSIX® 1003.1c [1]). Eine Programmierschnittstelle ist ganz allgemein eine Sammlung von Routinen, um eine bestimmte Funktionalität anderen Programmen zur Verfügung zu stellen. Oft wird dafür die Abkürzung API (engl. application programming interface) verwendet. Diese Sammlung von Routinen wird meist in Form einer Bibliothek implementiert. Eine API definiert im Gegensatz zu einer Binärschnittstelle ABI (engl. application binary interface) nur die Verwendung der Schnittstellen auf Quelltextebene. Die meisten API's sind in der Regel funktionsorientiert um z.B. eine Programmiersprache, in unserem Fall C/C++ um eine zusätzliche Funktionalität wie die Nebenläufigkeit von Prozessen zu erweitern. Da die Schnittstelle allgemein formuliert ist, gibt es verschiedene Implementierungen, die ähnlich aber nicht identisch sind. Die Pthread Bibliothek ist neben der Win32-Thread Bibliothek von Microsoft die am weitesten verbreitete Implementierung des 1995 definierten Standards, die im Gegensatz zur Microsoft Bibliothek auf den verschiedenen Betriebssystemen verfügbar ist.

 

Den verschiedenen Implementierungen ist gemeinsam, dass sie die Eigenschaft haben, sehr betriebssystemnahe Funktionen und Mechanismen zur Erzeugung, Synchronisation und zur Zugriffssteuerung der Threads verwenden. Bereits in den 80er Jahren wurden Threads in Multitasking-Betriebssystemen verwendet. Das bedeutet, die API orientiert sich an den Bedürfnissen von Betriebssystemprogrammierern und ist kompakt und effizient aber nicht unbedingt benutzerfreundlich. Zur Zugriffssteuerung der Threads werden ebenso die im Betriebssystem verwendeten Mechanismen, wie Mutexe und Semaphoren, verwendet. Ein Mutex verhindert, dass nebenläufige Prozesse bzw. Threads gleichzeitig auf Daten zugreifen und somit inkonsistente Zustände erzeugen. Eine Semaphore ist eine Datenstruktur zur Zugriffsverwaltung von Daten. Damit begibt man sich mit der Threadprogrammierung schon sehr tief in die Bereiche der Systemprogrammierung, was sich in der Aufrufkonvention der API widerspiegelt. Zum Thema Multithreading gibt es viel Literatur. Stellvertretend seien hier nur die Werke von Butendorf [2] und Lewis/Berg [3] erwähnt. Eine Implementierung der Pthread Bibliothek für Microsoft Windows findet sich unter [4]. Zusammenfassend ist zu sagen, dass man bei der Verwendung der Threadbibliotheken die volle Kontrolle über die Parallelisierung und Performance bei maximalem Aufwand zur Erzeugung und Kontrolle der Threads hat.

 

Ein Ausflug in die Pthread API

 

Wie bereits gesagt, muss sich der Programmierer bei Verwendung der Pthread API um die Anzahl der zur Laufzeit verwendeten Threads im Programm selbst kümmern. Zudem ist er für die Erzeugung der Threads selbst verantwortlich. Wie jedes sequentielle Programm besteht auch ein Programm, welches Threads verwendet, zunächst nur aus dem Hauptthread, in dem das Hauptprogramm und alle von diesem aufgerufene Funktionen definiert sind. Es können neue Threads erzeugt werden, wobei die durch den Thread durchzuführende Aufgabe in einer selbst zu schreibenden Funktion zu kapseln ist. Der Typ dieser Funktion ist ein Zeiger auf eine Funktion, die einen void Zeiger auf eine Funktion zurück gibt:

 

void * function( void *);

 

Das bedeutet, die Funktion muss eine Adresse als Argument bekommen und ebenfalls eine Adresse als Funktionsergebnis liefern. Erzeugt wird der neue Thread durch den Aufruf der Bibliotheksfunktion pthread_create:

 

int pthread_create (

pthread_t *ThreadID,

const pthread_attr_t *ThreadAttribute,

void *(*function) (void *),

void *arg

);

 

Im Detail haben die Parameter folgende Funktion: Die *ThreadId ist Identifikationsnummer des Threads. Mit *ThreadAttribute werden die Eigenschaften des neuen Threads definiert; wird hier NULL als Argument angegeben, so wird der Thread mit den Standard Attributen erzeugt. Der etwas komplex aussehende Ausdruck void *(*function)(void *) ist die Adresse der Funktion in der die parallele Arbeit gekapselt ist, die der Thread ausführen soll. Das vierte Argument *arg spezifiziert die Argumente, mit dem die durch den Thread zu bearbeitende Funktion *function aufgerufen werden soll. Nach dem erfolgreichen Aufruf von pthreat_create, wird das Ergebnis 0 zurückgeliefert. Damit beginnt die Lebenszeit des neuen Threads, in dem die angegebene Funktion ausgeführt wird. Die Lebenszeit des neuen Threads endet mit der Rückgabe des Returnwertes, dem Ende des Hauptprogramms oder von außen durch einen Thread API Aufruf (z.B. pthread_kill, pthread_cancel). Damit ergibt sich der folgende generische Programmrumpf ergeben:

 

#include <pthread.h> /* Include File mit Threading Definitionen */

 

void *function(void*)

{

/* Zu parallelisierende Arbeit */

}

 

main()

{

pthread_t ThreadId;

int ret_code;

ret_code = pthread_create (

pthread_t *ThreadID,

const pthread_attr_t *ThreadAttribute,

void *(*function) (void *),

void *arg

);

}

 

Damit lässt sich jetzt recht einfach ein Hello World Programm erstellen (siehe Hello World -Version 1). Allerdings fällt sofort ins Auge, wie viel Aufwand für die Erzeugung der Threads nötig ist. Die Parameterübergabe ist auch nicht wirklich simpel. Außerdem ist auch noch ein Fehler in dem Programm enthalten. Nach Aufruf des Programms bleibt der Bildschirm leer, obwohl wir die Ausgabe der print Statements erwarten. Der Grund ist, dass man noch ein Synchronisationskonstrukt benötigt, welches mit der Programmausführung solange wartet, bis alle Threads mit ihrer Arbeit fertig sind. In diesem Fall ist der Hauptthread mit seiner Arbeit fertig, bevor die anderen Threads aus ihren Funktionen zurückkehren und beendet damit abrupt alle Threads. Dieses Problem könnte man recht einfach mit einem Sleep() Aufruf lösen, eleganter und richtiger wäre ein pthread_join(tid, val_ptr) aus der Pthread API, um wirklich auf die Beendigung der Threads zu warten; das leistet Sleep() nur in diesem einfachen Fall. Im zweiten Beispiel (siehe Hello World -Version 2) werden vier Threads erzeugt und es wird vorschriftsmäßig auf alle gewartet. In diesem Beispiel wäre es jetzt noch interessant, wenn jeder Thread seine eigene ID ausgeben könnte. Eine erste Realisierung könnte folgendermaßen aussehen:

 

void *threadFunc(void *pArg) {

int* p = (int*)pArg;

int myNum = *p;

printf( “Thread number %d\n”, myNum);

}

. . .

/* from main(): */

pthread_t hThread[NUMTHREADS];

for (int i = 0; i < numThreads; i++) {

pthread_create(&hThread[i], NULL, threadFunc, &i);

}

 

Nehmen Sie sich ruhig ein wenig Zeit und schauen sich das Codefragment genauer an, bevor Sie weiter lesen. Das Problem ist nicht ganz offensichtlich, wenn man noch nie parallel programmiert hat und offenbart eine der wesentlichen Schwächen bei der Thread-Programmierung. Selbst simple Thread-Programmierung ist relativ kompliziert im Vergleich zur Programmierung von seriellen Programmen, da man immer die möglichen zeitlichen Abhängigkeiten vorhersagen muss. Warum das so problematisch ist, will ich im folgenden etwas näher beleuchten. Im obigen Beispiel kommt es zu folgendem Problem: Der Scheduling-Algorithmus entscheidet, wann und in welcher Reihenfolge die Instruktionen der verschiedenen Threads relativ zueinander ausgeführt werden und wann ein Thread eine Zustandsänderung des Speichers vornimmt. Solche Zustandsänderungen sind besonders kritisch, wenn mehrere Threads auf den gleichen Speicher zugreifen. In solchen Fällen kommt es zu so genannten Wettläufen (engl. race conditions), die in der Praxis unvorhersehbar und oft nicht reproduzierbar sind, da sie vom zeitabhängigen Verhalten des Schedulers abhängig sind. In unserem Fall ist es der Zugriff auf die Laufvariable i. In Abbildung 2 ist ein möglicher zeitlicher Verlauf für den Zugriff auf die Variable für zwei Threads dargestellt. Man sieht sehr gut, wie die Modifikation der Variablen und der zeitlich unbestimmte Zugriff die falschen Werte liefert. Es gibt mehrere Möglichkeiten data races zu verhindern. Dieses kann durch gegenseitigen Ausschluss (engl. mutual exclusion), durch lokale Kopien oder durch Sychronisationsprimitive erreicht werden. Die Möglichkeit mit lokalen Werten für die Variablen sieht folgendermaßen aus:

 

for (int i = 0; i < NUMTHREADS; i++) {
tNum[i] = i;
pthread_create(&hThread[i], NULL, helloFunc, &tNum[i]);
}

Damit bekommt jeder Thread seine eigene Kopie und eine ungewollte Modifikation ist ausgeschlossen.

 

 bild2.jpg

Abb. 1: Zeitlicher Ablauf des Zugriffs auf die Variable i

 

Synchronisationstechniken

 

Wie wir bisher gesehen haben, gibt es mehrere Möglichkeiten den Zugriff auf gemeinsame Ressourcen zu limitieren. Ein Weg ist es, lokale Kopien zu erzeugen, eine andere Möglichkeit wäre es, den Zugriff zu synchronisieren. Es gibt mehre Möglichkeiten dies zu tun, in dem man eine der folgenden Möglichkeiten wie Mutexe, Semphoren oder Locks wählt. Auf die Unterschiede wird später eingegangen. Wie schauen uns im nächsten Beispiel repräsentativ für die Synchronisationsprimitive den Mutex etwas genauer an. Zur Wiederholung sei gesagt, ein Mutex oder auch kritischer Bereich genannt, verhindert, dass nebenläufige Prozesse bzw. Threads gleichzeitig auf definierte Bereiche des Quellcodes zugreifen können. Der Mutex schützt diesen kritischen Bereich dadurch, dass dieser Bereich nur von einem Thread betreten werden kann. Ein Mutex hat nur zwei definierte Zustände: blockiert und frei. Vereinfacht kann man den Mutex auf folgende Art verwenden: Zuerst erzeugt und initialisiert man einen neuen Mutextyp (pthread_mutex_t) mit dem Aufruf der entsprechenden Funktion:

 

int pthread_mutex_init( mutex, attr )

 

Dieser Mutex ist zunächst nicht im Besitz eines bestimmten Threads. Durch Aufrufen der Funktion pthread_mutex_lock kann ein Thread versuchen, in Besitz des Mutex zu gelangen und diesen dann zu blockieren. Danach wird der Programmcode des geschützten Bereichs ausgeführt. Ist der Mutex blockiert und versucht ein weiterer Thread in Besitz des Mutexes zu gelangen, wird dieser Thread gestoppt. Wird der Mutex von dem blockierenden Thread freigegeben, bekommt der am längsten wartende Thread den Mutex. Damit ist sichergestellt, dass nur jeweils ein Thread nach dem anderen den durch den Mutex geschützten Bereich betritt. Ein Beispiel für die praktische Verwendung, den Schutz der Varibalen g_sum, könnte folgendermaßen aussehen:

 

#define NUMTHREADS 4

pthread_mutex_t gMutex;

int g_sum = 0;

 

void *threadFunc(void *arg)

{

int mySum = bigComputation();

pthread_mutex_lock( &gMutex );

g_sum += mySum;

pthread_mutex_unlock( &gMutex );

}

 

main() {

pthread_t hThread[NUMTHREADS];

 

pthread_mutex_init( &gMutex, NULL );

for (int i = 0; i < NUMTHREADS; i++)

pthread_create(&hThread[i],NULL,threadFunc,NULL);

 

for (int i = 0; i < NUMTHREADS; i++)

pthread_join(hThread[i]);

printf (“Global sum = %f\n”, g_sum);

}

 

Wichtig bei der Verwendung von Mutexen ist der paarweise Einsatz von lock/unlock, da sonst Deadlocks auftreten können [5]. Damit kann man einen Mutex als einfachen Ablaufzugriffs- Mechanismus für Ressourcen, hier: der gemeinsam benutzte Speicherbereich innerhalb des geschützten Bereichs, beschreiben. Häufig benötigt man aber aufwendigere Mechanismen zur Zugriffsteuerung. Zum Beispiel kann man sich vorstellen, dass man eine Methode benötigt, um Threads zu blockieren bis ein bestimmtes Ereignis eintritt. Dieser Mechanismus wird durch Bedingungsvariablen (engl. condition variable) realisiert. Ein weiterer Synchronisationsmechnismus ist die Semaphore, die ich hier noch kurz vorstellen möchte. Eine Semaphore bietet die Möglichkeit, den Zugriff der verschiedenen Threads auf eine exklusive Ressource abzustimmen. Semaphore kann man sich als ganzzahlige globale Variablen vorstellen, die für alle Threads gelten. Für ein bestimmtes Betriebsmittel wird Semaphore definiert und bekommt den Wert 1. Das bedeutet, dass Betriebsmittel ist verfügbar. Beabsichtigt nun ein Thread auf diese Ressource zuzugreifen, so überprüft er zunächst den Wert der Semaphore, bei 1 ist die Ressource verfügbar bei 0 nicht. Hat die Semaphore den Wert 1, wird diese von dem Thread dekrementiert und die Ressource kann verwendet werden. Beendet der Thread die Verwendung des Betriebsmittels, setzt er den Wert der Semaphore wieder auf 1. Essentiell ist hierbei, dass das Betriebssystem sicherstellt, dass der Vorgang das Testen der Semaphore und die dann ggf. notwendige Dekrementierung atomar (in einem Schritt) durchführt, damit kein anderer Thread diesen Vorgang stören und damit eine inkonsistente Zustandsänderung erzeugen kann. Es gibt noch weitere Synchronisationsprimitive, die im Rahmen dieses Artikels nicht weiter behandelt werden, da das Prinzip jetzt sicher klar ist.

 

Synchronisation im Detail

 

Wie sicher mittlerweile deutlich geworden ist, ist das Schlüsselproblem bei der parallelen Programmierung die Synchronisation. Um Zustände, die zu data races führen, zu vermeiden, kann man verschiedene Möglichkeiten wählen, um das gegenseitige Überschreiben der Speicherbereiche zu vermeiden. Dabei liegt der Schlüssel zu einer performanten Ausführungsgeschwindigkeit in der Minimierung des Synchronisationsoverheads. Interessant ist nun die Frage, warum dieser Aufwand denn überhaupt nötig ist und was die Crux bei der parallelen Programmierung bzw. der Synchronisation ist. Wenn man sich eine einfache Inkrement-Anweisung anschaut, sollte diese Anweisung doch eigentlich atomar, also in einem Schritt ablaufen und überhaupt keine Probleme bereiten.

 

i++;

 

Dies ist aber nicht so. Denn aus

 

i++; oder besser i=i+1;

 

werden die zugehörigen Assemblerbefehle, die das Problem deutlich machen.

 

mov eax, dword ptr[i]

inc eax

mov dword ptr[i], eax

 

In diesem Fall wird selbst die simple Inkrement-Operation in mehrere Assembler Instruktionen aufgeteilt. Damit es nicht zu Problemen bei der Synchronisation kommen kann, müsste aber die Inkrement-Operation atomar sein. Atomar bezeichnet hier eine Operation, welche durch keine andere Operation unterbrochen werden kann. Man könnte versuchen dies mit Inline-Assemblerbefehlen zu realisieren:

 

__asm__ __volatile__ (
  "incl %0\n\t"
  :"=m"(i)
  :"m"(i)
);

Dieses Vorgehen würde auf einer CPU mit mehreren Threads das richtige Ergebnis liefern. Bei mehreren CPUs kommt es weiterhin zu Wettläufen, weil das Speichersubsystem lediglich sicherstellt, dass jeweils eine CPU zu einem bestimmten Zeitpunkt die Möglichkeit bekommt von einer Speicherstelle zu lesen. Aber sobald dieser Lesevorgang beendet ist, kann jede andere CPU von derselben Speicherstelle lesen oder schreiben ohne darauf Rücksicht zu nehmen, dass die RMW (engl. read-modify-write) Sequenz der ersten CPU beendet ist. Wie kann man nun dieses Dilemma lösen ? Auf modernen CPUs gibt es das Instruktionsprefix Lock, also eine Zugriffssperre (engl. Lock), die man in der Regel als eine generische Semaphore implementiert und mit der man bestimmte Assemblerbefehle in atomare Befehle umwandeln kann. Dies würde dann in Inline-Assembler folgendermaßen aussehen:

 

__asm__ __volatile__ (

"lock; incl %0\n\t"

:"=m"(i)

:"m"(i)

);


Atomare Operationen sind essentiell beim Synchronisieren von Daten. Aus diesem Grund stellen die verschiedenen Betriebssysteme Mechanismen zur Verfügung, die atomare Operationen für bestimmte einfache Funktionen ohne Inline-Assembler implementieren. Unter Linux wäre diese z.B.:

 

int atomic_inc_and_test(atomic_t * v);

 

also

 

atomic_inc_and_test(&i); /* ~ i++ */

 

Schaut man sich den dazugehörige Assemblercode an

 

lock add dword ptr[i],i

 

fällt sofort auf, dass nun die Operation mit einem Lock geschützt ist. Wie genau jetzt diese Lock Operation implementiert ist, würde den Rahmen dieses Artikels sprengen. Wenn man sich die Programmlaufzeit mit Lock anschaut, stellt man fest, dass diese gegenüber der ohne Lock länger geworden ist. Dies ist natürlich offensichtlich, da durch den Lock-Mechanismus ein Verwaltungsoverhead in der CPU realisiert werden muss. Wichtig ist nur, dass die atomaren Lock-Operationen in der Regel den kleinsten Overhead besitzen. Wie sieht das nun aber für die anderen Operationen aus, die man verwenden kann? Eine einfache qualitative Abschätzung liefert folgender Überblick:

 

Synchronisationsobjekt

1 CPU

Mehrere CPUs (Kerne)

Ohne Kontrolle

1(Basis)

1.5x

Atomarer Inkrement

15x

10x

Mutex

150x

200x

Semaphore

160x

210x

 

In der Tabelle 1 sind die Werte für den Aufruf einer leeren Funktion dargestellt. Man sieht deutlich, dass durch die Verwendung von komplexen Synchronisationsprimitiven der Overhead schon mal ca. 200 mal größer ist also ohne Synchronisation. Unglücklicherweise gibt es in der Regel nur für relativ einfache Operationen atomare Aufrufe, es macht aber durchaus Sinn diese zu favorisieren.

 

Dabei soll an dieser Stelle nicht verschwiegen werden, dass es seit geraumer Zeit auch Untersuchungen zu nicht-blockierenden Synchronisationstechniken gibt. Dabei gibt es verschiedene Herangehensweisen. Entweder werden Algorithmen in nicht-blockierende Algorithmen transformiert oder es werden Datenstrukturen verwendet, die sich ohne Blockierung synchronisieren lassen. Die Beschreibung dieses Konzepts würde aber einen eigenen Artikel bedingen und auch nicht in das hier vorgestellte Konzept passen.

Zusammenfassung

Die Verwendung einer Threading Bibliothek, wie in diesem Artikel am Beispiel der Posix Library Pthreads dargestellt, ist Compiler- und plattformunabhängig. Mit dem Konzept der Pthread Bibliothek können Multithread-Anwendungen sehr feingranular und systemnah erzeugt werden. Dabei liegt der Schwerpunkt auf einer maximalen Kontrolle, die eine möglichst hohe Performance bedeuten kann, eine optimale Implementierung vorausgesetzt, da man die volle Kontrolle über die verschiedenen Aspekte der Parallelisierung besitzt (Erzeugung der Threads, Synchronisation, ...) und sich nicht in die Hände einer Laufzeitumgebung wie bei OpenMP begibt. Allerdings ist dieser Ansatz weder abstrakt noch einfach, da die verwendete API relativ User-unfreundlich implementiert ist, was der Zeit ihrer Implementierung (1995!) und der primären Verwendung in der Betriebssystemprogrammierung geschuldet ist. Dazu kommen noch all die Herausforderungen (deadlocks, data races, ...) die eine parallele Implementierung gegenüber ihrem seriellen Pendant stellt. Abschließend lässt sich sagen, dass bei der Programmierung von parallelen Anwendungen, die Programmierung mit einer Threading Bibliothek die höchsten Anforderungen an den Programmentwurf und an die Fähigkeiten der Entwickler stellt, allerdings mit der Aussicht auf maximale Performance.

 

Hello World – Version 1

1 #include <stdio.h>

2 #include <pthread.h>

3

4

5 void *helloFunc(void *pArg) ;

6

7 void *helloFunc(void *pArg)

8 {

9 printf("Hello Thread\n");

10 }

11

12 int main()

13 {

14 pthread_t hThread;

15

16 pthread_create(&hThread, NULL, helloFunc, NULL);

17

18 return 1;

19 }

 

Übersetzen (Linux): gcc -Wall -g -o hello1 hello1.c -lpthread

Ausführung: ./hello1
>

Hello World – Version 2

 

hello.c

 

1 #include <stdio.h>

2 #include <pthread.h>

3

4 #define NUMTHREADS 4

5

6 void *helloFunc(void *pArg) ;

7

8 void *helloFunc(void *pArg)

9 {

10 printf("Hello Thread\n");

11 }

12

13 int main()

14 {

15 int i,j ;

16 pthread_t hThread[NUMTHREADS];

17

18 for (i = 0; i < NUMTHREADS; i++)

19 pthread_create(&hThread[i], NULL, helloFunc, NULL);

20

21 for (j = 0; j < NUMTHREADS; j++)

22 pthread_join(hThread[j], NULL);

23

24 return 1;

25 }

26

 

Übersetzen (Linux): gcc -Wall -g -o hello2 hello2.c -lpthread

Ausführung: ./hello2

>
Hello Thread

Hello Thread

Hello Thread

Hello Thread

 

Dr. Mario Deilmann ist Diplom-Ingenieur und hat an der Ruhr-Universität-Bochum im Bereich numerische Simulation promoviert. Er arbeitet seit 2003 als Senior Software Engineer bei der Firma Intel und ist im Bereich des technischen Consulting für die Intel® Compiler Gruppe tätig.

Links & Literatur

[1] ISO / IEC 9945-1:1996(E) ANSI / IEEE Std 1003.1, 1996 Edition, Information technology Portable Operating System Interface (POSIX®) - Part 1: System Application Program Interface (API) [C Language]

[2] David R. Butenhof: Programming with POSIX® Threads, Addison-Wesley

[3] Bil Lewis, Daniel J. Berg: Multithreaded Programming with Pthreads, Sun Press, 1998

          [4] POSIX Threads für Win32 Systeme - http://sourceware.org/pthreads-win32/

          [5] M. Deilman, Programmiermodelle für moderne Mehrkernarchitekturen, Der Entwickler 1/2007

 

Para obtener información más completa sobre las optimizaciones del compilador, consulte nuestro Aviso de optimización.