1 Einleitung
Die Thread-Technologie wurde bereits in den 1960er Jahren vorgeschlagen, Multithreading wurde jedoch erst Mitte der 1980er Jahre wirklich auf Betriebssysteme angewendet, und Solaris war in dieser Hinsicht führend. Traditionelles Unix unterstützt auch das Konzept von Threads, aber in einem Prozess ist nur ein Thread zulässig, sodass Multithreading Multiprozess bedeutet. Mittlerweile wird die Multithreading-Technologie von vielen Betriebssystemen unterstützt, darunter Windows/NT und natürlich Linux. Warum müssen wir Threads einführen, nachdem wir das Konzept von Prozessen haben? Welche Vorteile bietet die Verwendung von Multithreading? Welche Art von System sollte Multithreading wählen? Diese Fragen müssen wir zunächst beantworten. Einer der Gründe für die Verwendung von Multithreading besteht darin, dass es im Vergleich zu Prozessen eine sehr „sparsame“ Art des Multitaskings ist. Wir wissen, dass unter Linux beim Starten eines neuen Prozesses diesem ein unabhängiger Adressraum zugewiesen werden muss und zahlreiche Datentabellen erstellt werden müssen, um seinen Codeabschnitt, seinen Stapelabschnitt und seinen Datenabschnitt zu verwalten. Dies ist eine „teure“ Art, mit mehreren Aufgaben zu arbeiten. Mehrere Threads, die in einem Prozess ausgeführt werden, verwenden denselben Adressraum und teilen sich die meisten Daten. Der zum Starten eines Threads benötigte Speicherplatz ist viel kleiner als der zum Starten eines Prozesses benötigte Speicherplatz. Darüber hinaus ist die zum Wechseln zwischen Threads benötigte Zeit viel kürzer als die zum Wechseln zwischen Prozessen benötigte Zeit. Laut Statistik beträgt der Overhead eines Prozesses im Allgemeinen etwa das 30-fache des Overheads eines Threads. Natürlich können diese Daten auf einem bestimmten System stark abweichen. Der zweite Grund für die Verwendung von Multithreading ist der praktische Kommunikationsmechanismus zwischen Threads. Verschiedene Prozesse verfügen über unabhängige Datenräume und Daten können nur durch Kommunikation übertragen werden, was nicht nur zeitaufwändig, sondern auch unpraktisch ist. Dies ist bei Threads nicht der Fall. Da Threads im selben Prozess Datenraum gemeinsam nutzen, können die Daten eines Threads direkt von anderen Threads verwendet werden, was nicht nur schnell, sondern auch praktisch ist. Natürlich bringt die gemeinsame Nutzung von Daten auch andere Probleme mit sich. Einige Variablen können nicht von zwei Threads gleichzeitig geändert werden. Einige in Unterprogrammen als statisch deklarierte Daten können Multithread-Programmen mit größerer Wahrscheinlichkeit katastrophale Auswirkungen zufügen. Dies sind die Bereiche, die beim Schreiben von Multithread-Programmen die größte Aufmerksamkeit erfordern. Neben den oben genannten Vorteilen bieten Multithread-Programme als Multitasking- und Parallelarbeitsmethode gegenüber Prozessen sicherlich noch folgende Vorteile: 1) Verbessern Sie die Reaktionsfähigkeit der Anwendung. Dies ist insbesondere für Programme mit grafischer Oberfläche von Bedeutung. Wenn ein Vorgang lange dauert, wartet das gesamte System auf diesen Vorgang. Zu diesem Zeitpunkt reagiert das Programm nicht auf Tastatur-, Maus- oder Menüoperationen. Durch die Verwendung der Multithreading-Technologie, um zeitaufwändige Vorgänge in einem neuen Thread zu platzieren, kann diese peinliche Situation vermieden werden. 2) Machen Sie Multi-CPU-Systeme effizienter. Das Betriebssystem stellt sicher, dass, wenn die Anzahl der Threads nicht größer als die Anzahl der CPUs ist, unterschiedliche Threads auf unterschiedlichen CPUs ausgeführt werden. 3) Verbessern Sie die Programmstruktur. Ein langer und komplexer Prozess kann in mehrere Threads aufgeteilt werden, die dann zu mehreren unabhängigen oder halbunabhängigen laufenden Teilen werden können. Ein solches Programm ist leichter zu verstehen und zu ändern. Versuchen wir, ein einfaches Multithread-Programm zu schreiben.
2 Einfache Multithread-Programmierung
Multithreading unter dem Linux-System folgt der POSIX-Thread-Schnittstelle, die pthread genannt wird. Um ein Multithread-Programm unter Linux zu schreiben, müssen Sie beim Verknüpfen die Header-Datei pthread.h und die Bibliothek libpthread.a verwenden. Die Implementierung von pthread unter Linux wird übrigens durch den Systemaufruf clone() erreicht. clone() ist ein Systemaufruf, der nur in Linux vorkommt. Seine Verwendung ähnelt der von fork. Weitere Einzelheiten zu clone() finden interessierte Leser in der entsprechenden Dokumentation. Nachfolgend zeigen wir das einfachste Multithread-Programm example1.c.
/* beispiel.c */
#include <stdio.h>
#include <pthread.h>
Ungültiger Thread (ungültig)
{
int ich;
für (i = 0; i < 3; i++)
printf("Dies ist ein pthread./n");
}
int Haupt(void)
{
pthread_t-ID;
int i, ret;
ret = pthread_create(&id,NULL,(void *) thread,NULL);
wenn(ret!=0)
{
printf ("Fehler beim Erstellen von Pthread!/n");
Ausgang (1);
}
für (i = 0; i < 3; i++)
printf("Dies ist der Hauptprozess./n");
pthread_join(id,NULL);
Rückgabe (0);
}
Wir stellen dieses Programm zusammen: gcc beispiel1.c -lpthread -o beispiel1 Wenn wir Beispiel 1 ausführen, erhalten wir die folgenden Ergebnisse:
Dies ist der Hauptvorgang. Dies ist ein Pthread. Dies ist der Hauptvorgang. Dies ist der Hauptvorgang. Dies ist ein Pthread. Dies ist ein Pthread.
Bei erneutem Ausführen erhalten wir möglicherweise die folgenden Ergebnisse:
Dies ist ein Pthread. Dies ist der Hauptvorgang. Dies ist ein Pthread. Dies ist der Hauptvorgang. Dies ist ein Pthread. Dies ist der Hauptvorgang.
Die beiden Ergebnisse sind unterschiedlich. Dies ist darauf zurückzuführen, dass zwei Threads um CPU-Ressourcen konkurrieren. Im obigen Beispiel haben wir zwei Funktionen verwendet, pthread_create und pthread_join, und eine Variable vom Typ pthread_t deklariert. pthread_t ist in der Header-Datei /usr/include/bits/pthreadtypes.h definiert: Typdefinition: vorzeichenlos, lange Int. pthread_t; Es ist ein Bezeichner eines Threads. Die Funktion pthread_create dient zum Erstellen eines Threads. Ihr Prototyp ist: extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr, void *(*__start_routine) (void *), void *__arg)); Der erste Parameter ist ein Zeiger auf die Thread-Kennung, der zweite Parameter wird zum Festlegen der Thread-Attribute verwendet, der dritte Parameter ist die Startadresse der Thread-Ausführungsfunktion und der letzte Parameter sind die Parameter der Ausführungsfunktion. Hier benötigt unser Funktionsthread keine Parameter, daher wird der letzte Parameter auf einen Nullzeiger gesetzt. Wir setzen auch den zweiten Parameter auf einen Nullzeiger, wodurch ein Thread mit Standardeigenschaften generiert wird. Im nächsten Abschnitt erklären wir, wie Sie Thread-Attribute festlegen und ändern. Wenn der Thread erfolgreich erstellt wurde, gibt die Funktion 0 zurück. Wenn der Wert nicht 0 ist, bedeutet dies, dass die Thread-Erstellung fehlgeschlagen ist. Häufige Fehlerrückgabecodes sind EAGAIN und EINVAL. Ersteres bedeutet, dass das System die Erstellung neuer Threads einschränkt, z. B. wenn die Anzahl der Threads zu groß ist; letzteres bedeutet, dass der durch den zweiten Parameter dargestellte Thread-Attributwert unzulässig ist. Nachdem der Thread erfolgreich erstellt wurde, führt der neu erstellte Thread die durch die Parameter drei und vier bestimmte Funktion aus und der ursprüngliche Thread fährt mit der Ausführung der nächsten Codezeile fort. Die Funktion pthread_join wird verwendet, um auf das Ende eines Threads zu warten. Der Funktionsprototyp ist: extern int pthread_join __P ((pthread_t __th, void **__thread_return)); Der erste Parameter ist die Kennung des Threads, auf den gewartet wird, und der zweite Parameter ist ein benutzerdefinierter Zeiger, der zum Speichern des Rückgabewerts des Threads, auf den gewartet wird, verwendet werden kann. Diese Funktion ist eine Thread-Blockierungsfunktion. Die Funktion, die sie aufruft, wartet, bis der wartende Thread beendet ist. Wenn die Funktion zurückkehrt, werden die Ressourcen des wartenden Threads zurückgefordert. Es gibt zwei Möglichkeiten, einen Thread zu beenden. Eine ist wie im obigen Beispiel: Wenn die Funktion endet, endet auch der Thread, der sie aufgerufen hat. Die andere Möglichkeit besteht darin, dies über die Funktion pthread_exit zu implementieren. Sein Funktionsprototyp ist: extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__)); Das einzige Argument ist der Rückgabecode der Funktion, der an thread_return übergeben wird, solange das zweite Argument von pthread_join, thread_return, nicht NULL ist. Abschließend ist zu beachten, dass nicht mehrere Threads auf einen Thread warten können, da sonst der erste Thread, der das Signal empfängt, erfolgreich zurückkehrt und die verbleibenden Threads, die pthread_join aufrufen, den Fehlercode ESRCH zurückgeben. In diesem Abschnitt haben wir den einfachsten Thread geschrieben und die drei am häufigsten verwendeten Funktionen pthread_create, pthread_join und pthread_exit gemeistert. Als Nächstes lernen wir einige allgemeine Eigenschaften von Threads kennen und wie man sie festlegt. 3 Thread-Eigenschaften ändern
Im Beispiel im vorherigen Abschnitt haben wir die Funktion pthread_create verwendet, um einen Thread zu erstellen. In diesem Thread haben wir die Standardparameter verwendet, d. h. der zweite Parameter der Funktion wurde auf NULL gesetzt. Tatsächlich reicht es für die meisten Programme aus, die Standardeigenschaften zu verwenden. Dennoch ist es notwendig, dass wir die relevanten Eigenschaften von Threads verstehen. Die Attributstruktur ist pthread_attr_t, die auch in der Header-Datei /usr/include/pthread.h definiert ist. Wer der Sache auf den Grund gehen möchte, kann sich das gerne selbst anschauen. Der Attributwert kann nicht direkt festgelegt werden und muss mithilfe verwandter Funktionen bedient werden. Die Initialisierungsfunktion ist pthread_attr_init, die vor der Funktion pthread_create aufgerufen werden muss. Zu den Attributobjekten gehören hauptsächlich Angaben dazu, ob gebunden oder getrennt werden soll, die Stapeladresse, die Stapelgröße und die Priorität. Die Standardattribute sind „ungebunden“, „nicht getrennt“, „Standard-Stapel“ von 1 MB und dieselbe Prioritätsstufe wie der übergeordnete Prozess. Bei der Fadenbindung kommt ein weiteres Konzept zum Einsatz: Light Weight Process (LWP). Ein leichter Prozess kann als Kernel-Thread verstanden werden, der zwischen der Benutzerebene und der Systemebene angesiedelt ist. Das System weist Thread-Ressourcen zu und steuert Threads über leichte Prozesse. Ein leichter Prozess kann einen oder mehrere Threads steuern. Standardmäßig steuert das System, wie viele Light-Prozesse gestartet werden und welche Light-Prozesse welche Threads steuern. Diese Situation wird als ungebunden bezeichnet. Im bindenden Zustand wird, wie der Name schon sagt, ein Faden fest an einen Lichtfortsatz „gebunden“. Der gebundene Thread hat eine höhere Antwortgeschwindigkeit, da die Planung der CPU-Zeitscheiben auf leichte Prozesse ausgerichtet ist. Der gebundene Thread kann sicherstellen, dass bei Bedarf immer ein leichter Prozess verfügbar ist. Durch Festlegen der Priorität und der Planungsebene des gebundenen Lichtprozesses kann der gebundene Thread Anforderungen wie Echtzeitreaktion erfüllen. Die Funktion, die den Thread-Bindungsstatus festlegt, ist pthread_attr_setscope und hat zwei Parameter. Der erste ist ein Zeiger auf die Attributstruktur und der zweite ist der Bindungstyp, der zwei Werte hat: PTHREAD_SCOPE_SYSTEM (gebunden) und PTHREAD_SCOPE_PROCESS (ungebunden). Der folgende Code erstellt einen gebundenen Thread.
#include <pthread.h>
pthread_attr_t-Attr;
pthread_t-Tid;
/* Eigenschaftswerte initialisieren, alle auf Standardwerte setzen */
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void *) meine_Funktion, NULL);
Der getrennte Zustand eines Threads bestimmt, wie ein Thread sich selbst beendet. Im obigen Beispiel verwenden wir die Standardeigenschaft des Threads, nämlich den nicht getrennten Zustand. In diesem Fall wartet der ursprüngliche Thread, bis der erstellte Thread beendet ist. Erst wenn die Funktion pthread_join() zurückkehrt, wird der erstellte Thread beendet und kann die von ihm belegten Systemressourcen freigeben. Bei einem abgetrennten Thread ist das nicht der Fall. Auf ihn warten keine anderen Threads. Wenn seine eigene Ausführung abgeschlossen ist, wird der Thread beendet und die Systemressourcen werden sofort freigegeben. Programmierer sollten den geeigneten Trennungszustand entsprechend ihren Anforderungen auswählen. Die Funktion zum Festlegen des Thread-Trennstatus ist pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate). Der zweite Parameter kann PTHREAD_CREATE_DETACHED (getrennter Thread) und PTHREAD _CREATE_JOINABLE (nicht getrennter Thread) sein. Zu beachten ist hierbei, dass ein Thread, der als separater Thread festgelegt ist und sehr schnell läuft, wahrscheinlich beendet wird, bevor die Funktion pthread_create zurückkehrt. Nach der Beendigung überträgt er möglicherweise die Threadnummer und die Systemressourcen zur Verwendung an andere Threads. Auf diese Weise erhält der Thread, der pthread_create aufruft, die falsche Threadnummer. Um diese Situation zu vermeiden, können Sie bestimmte Synchronisierungsmaßnahmen ergreifen. Eine der einfachsten Methoden besteht darin, die Funktion pthread_cond_timewait im erstellten Thread aufzurufen, um den Thread eine Weile warten zu lassen und genügend Zeit für die Rückkehr der Funktion pthread_create zu lassen. Das Festlegen einer Wartezeit ist eine häufig verwendete Methode in der Multithread-Programmierung. Achten Sie jedoch darauf, keine Funktionen wie wait() zu verwenden, die den gesamten Prozess in den Ruhezustand versetzen und das Problem der Thread-Synchronisierung nicht lösen können. Ein weiteres häufig verwendetes Attribut ist die Thread-Priorität, die in der Struktur sched_param gespeichert ist. Verwenden Sie zum Speichern die Funktionen pthread_attr_getschedparam und pthread_attr_setschedparam. Im Allgemeinen nehmen wir immer zuerst die Priorität, ändern den erhaltenen Wert und speichern ihn dann wieder. Das Folgende ist ein einfaches Beispiel.
/* beispiel.c */
#include <stdio.h>
#include <pthread.h>
Ungültiger Thread (ungültig)
{
int ich;
für (i = 0; i < 3; i++)
printf("Dies ist ein pthread./n");
}
int Haupt(void)
{
pthread_t-ID;
int i, ret;
ret = pthread_create(&id,NULL,(void *) thread,NULL);
wenn(ret!=0)
{
printf ("Fehler beim Erstellen von Pthread!/n");
Ausgang (1);
}
für (i = 0; i < 3; i++)
printf("Dies ist der Hauptprozess./n");
pthread_join(id,NULL);
Rückgabe (0);
}
4-Thread-Datenverarbeitung
Im Vergleich zu Prozessen ist die gemeinsame Nutzung von Daten einer der größten Vorteile von Threads. Jeder Prozess nutzt das vom übergeordneten Prozess geerbte Datensegment gemeinsam und kann Daten problemlos abrufen und ändern. Dies bringt jedoch auch viele Probleme bei der Multithread-Programmierung mit sich. Wir müssen vorsichtig sein, wenn mehrere verschiedene Prozesse auf die gleiche Variable zugreifen. Viele Funktionen sind nicht reentrant, d. h., es können nicht mehrere Kopien einer Funktion gleichzeitig ausgeführt werden (es sei denn, es werden unterschiedliche Datensegmente verwendet). In Funktionen deklarierte statische Variablen verursachen häufig Probleme, und auch Funktionsrückgabewerte können Probleme verursachen. Denn wenn die Adresse des statisch deklarierten Bereichs innerhalb der Funktion zurückgegeben wird, kann ein anderer Thread diese Funktion aufrufen und diese Daten ändern, wenn ein Thread die Funktion aufruft, um die Adresse abzurufen und die von der Adresse gezeigten Daten verwendet. Innerhalb eines Prozesses gemeinsam genutzte Variablen müssen mit dem Schlüsselwort volatile definiert werden, um zu verhindern, dass der Compiler ihre Verwendungsweise während der Optimierung ändert (z. B. durch Verwendung des Parameters -OX in gcc). Um Variablen zu schützen, müssen wir Semaphoren, Mutexe und andere Methoden verwenden, um sicherzustellen, dass wir Variablen richtig verwenden. Als Nächstes werden wir schrittweise die relevanten Kenntnisse zur Verarbeitung von Thread-Daten vorstellen. 4.1 Gewindedaten
In einem Singlethread-Programm gibt es zwei grundlegende Datentypen: globale Variablen und lokale Variablen. In Multithread-Programmen gibt es jedoch einen dritten Datentyp: Thread-Daten (TSD: Thread-Specific Data). Es ist einer globalen Variable sehr ähnlich. Innerhalb eines Threads kann es von jeder Funktion wie eine globale Variable aufgerufen werden, es ist jedoch für andere Threads außerhalb des Threads nicht sichtbar. Der Bedarf an solchen Daten liegt auf der Hand. Beispielsweise gibt unsere allgemeine Variable errno Standardfehlerinformationen zurück. Es kann offensichtlich keine lokale Variable sein, da fast jede Funktion in der Lage sein sollte, sie aufzurufen; es kann aber auch keine globale Variable sein, da sonst die in Thread A ausgegebene Fehlermeldung wahrscheinlich die von Thread B ist. Um Variablen wie diese zu implementieren, müssen wir Thread-Daten verwenden. Wir erstellen für jeden Thread einen Schlüssel, der mit diesem Schlüssel verknüpft ist. In jedem Thread wird dieser Schlüssel verwendet, um auf die Thread-Daten zu verweisen, aber in verschiedenen Threads sind die durch diesen Schlüssel dargestellten Daten unterschiedlich. Im selben Thread stellt er denselben Dateninhalt dar. Es gibt vier Hauptfunktionen im Zusammenhang mit Thread-Daten: Erstellen eines Schlüssels; Zuweisen von Thread-Daten zu einem Schlüssel; Lesen von Thread-Daten aus einem Schlüssel; und Löschen eines Schlüssels. Der Funktionsprototyp zum Erstellen eines Schlüssels lautet: extern int pthread_key_create __P ((pthread_key_t *__key, ungültig (*__destr_function) (void *))); Der erste Parameter ist ein Zeiger auf einen Schlüsselwert, und der zweite Parameter gibt eine Destruktorfunktion an. Wenn dieser Parameter nicht leer ist, ruft das System diese Funktion auf, um den an diesen Schlüssel gebundenen Speicherblock freizugeben, wenn jeder Thread endet. Diese Funktion wird oft zusammen mit der Funktion pthread_once ((pthread_once_t*once_control, void (*initroutine) (void))) verwendet, um sicherzustellen, dass der Schlüssel nur einmal erstellt wird. Die Funktion pthread_once deklariert eine Initialisierungsfunktion. Beim ersten Aufruf von pthread_once wird diese Funktion ausgeführt. Nachfolgende Aufrufe werden von ihr ignoriert.
Im folgenden Beispiel erstellen wir einen Schlüssel und verknüpfen ihn mit einigen Daten. Wir müssen eine Funktion createWindow definieren, die ein Grafikfenster definiert (der Datentyp ist Fl_Window*, der Datentyp im grafischen Schnittstellenentwicklungstool FLTK). Da jeder Thread diese Funktion aufruft, verwenden wir Thread-Daten.
/* beispiel.c */
#include <stdio.h>
#include <pthread.h>
Ungültiger Thread (ungültig)
{
int ich;
für (i = 0; i < 3; i++)
printf("Dies ist ein pthread./n");
}
int Haupt(void)
{
pthread_t-ID;
int i, ret;
ret = pthread_create(&id,NULL,(void *) thread,NULL);
wenn(ret!=0)
{
printf ("Fehler beim Erstellen von Pthread!/n");
Ausgang (1);
}
für (i = 0; i < 3; i++)
printf("Dies ist der Hauptprozess./n");
pthread_join(id,NULL);
Rückgabe (0);
}
Auf diese Weise kann durch Aufrufen der Funktion createMyWin in verschiedenen Threads die Fenstervariable abgerufen werden, die im Thread sichtbar ist. Diese Variable wird über die Funktion pthread_getspecific abgerufen. Im obigen Beispiel haben wir die Funktion pthread_setspecific verwendet, um Thread-Daten an einen Schlüssel zu binden. Die Prototypen dieser beiden Funktionen sind wie folgt: extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer)); extern void *pthread_getspecific __P ((pthread_key_t __key)); Die Bedeutung und Verwendung der Parameter dieser beiden Funktionen sind offensichtlich. Es ist zu beachten, dass Sie beim Verwenden von pthread_setspecific zum Angeben neuer Thread-Daten für einen Schlüssel die ursprünglichen Thread-Daten selbst freigeben müssen, um Speicherplatz freizugeben. Diese Prozessfunktion pthread_key_delete wird zum Löschen eines Schlüssels verwendet. Der von diesem Schlüssel belegte Speicher wird freigegeben. Es ist jedoch auch zu beachten, dass nur der vom Schlüssel belegte Speicher freigegeben wird, nicht jedoch die von den mit dem Schlüssel verknüpften Threaddaten belegten Speicherressourcen. Außerdem wird die in der Funktion pthread_key_create definierte Destruktorfunktion nicht ausgelöst. Die Freigabe der Thread-Daten muss vor dem Loslassen der Taste abgeschlossen sein. 4.2 Mutex-Sperren
Mutex-Sperren werden verwendet, um sicherzustellen, dass immer nur ein Thread einen Codeabschnitt ausführt. Die Notwendigkeit liegt auf der Hand: Wenn jeder Thread nacheinander Daten in dieselbe Datei schreibt, ist das Endergebnis katastrophal. Schauen wir uns zunächst den folgenden Code an. Dies ist ein Lese-/Schreibprogramm, das einen gemeinsamen Puffer verwendet, und wir gehen davon aus, dass ein Puffer nur eine Information enthalten kann. Das heißt, der Puffer hat nur zwei Zustände: mit Informationen oder ohne Informationen.
void Leserfunktion ( void );
void Schreibfunktion ( void );
Zeichenpuffer;
int Puffer hat Element = 0;
pthread_mutex_t Mutex;
Struktur Zeitspezifikationsverzögerung;
void main ( void ) {
pthread_t-Leser;
/* Verzögerungszeit definieren */
Verzögerung.tv_sec = 2;
Verzögerung.tv_nec = 0;
/* Initialisiere ein Mutex-Objekt mit Standardeigenschaften */
pthread_mutex_init (&mutex,NULL);
pthread_create(&reader, pthread_attr_default, (void *)&reader_function), NULL);
Schreibfunktion();
}
void Schreibfunktion (void){
während(1){
/* Mutex sperren */
pthread_mutex_lock (&mutex);
wenn (Puffer hat Element == 0) {
Puffer = neues_Element erstellen();
Puffer hat Element = 1;
}
/* Öffne das Mutex */
pthread_mutex_unlock(&mutex);
pthread_delay_np(&Verzögerung);
}
}
void Leserfunktion(void){
während(1){
pthread_mutex_lock(&mutex);
wenn(Puffer_hat_Element==1){
verbrauche_Artikel(Puffer);
Puffer hat Element = 0;
}
pthread_mutex_unlock(&mutex);
pthread_delay_np(&Verzögerung);
}
}
Hier wird die Mutex-Variable mutex deklariert. Die Struktur pthread_mutex_t ist ein privater Datentyp, der ein vom System zugewiesenes Attributobjekt enthält. Die Funktion pthread_mutex_init wird verwendet, um eine Mutex-Sperre zu generieren. Ein NULL-Parameter gibt an, dass die Standardeigenschaften verwendet werden. Wenn Sie ein Mutex mit bestimmten Attributen deklarieren müssen, müssen Sie die Funktion pthread_mutexattr_init aufrufen. Die Funktionen pthread_mutexattr_setpshared und pthread_mutexattr_settype werden zum Festlegen von Mutex-Attributen verwendet. Die vorherige Funktion legt die Eigenschaft pshared fest, die zwei Werte hat: PTHREAD_PROCESS_PRIVATE und PTHREAD_PROCESS_SHARED. Ersteres wird zum Synchronisieren von Threads in verschiedenen Prozessen verwendet, und Letzteres wird zum Synchronisieren verschiedener Threads im selben Prozess verwendet. Im obigen Beispiel haben wir die Standardeigenschaft PTHREAD_PROCESS_PRIVATE verwendet. Letzteres wird verwendet, um den Mutex-Sperrtyp festzulegen. Die optionalen Typen sind PTHREAD_MUTEX_NORMAL, PTHREAD_MUTEX_ERRORCHECK, PTHREAD_MUTEX_RECURSIVE und PTHREAD _MUTEX_DEFAULT. Sie definieren jeweils unterschiedliche Auflistungs- und Entsperrmechanismen. Im Allgemeinen wird das letzte Standardattribut ausgewählt. Die Anweisung pthread_mutex_lock beginnt mit dem Sperren des Mutex, und der gesamte nachfolgende Code ist gesperrt, bis pthread_mutex_unlock aufgerufen wird, d. h. er kann immer nur von einem Thread gleichzeitig aufgerufen und ausgeführt werden. Wenn ein Thread pthread_mutex_lock ausführt und die Sperre zu diesem Zeitpunkt von einem anderen Thread verwendet wird, wird der Thread blockiert. Das heißt, das Programm wartet, bis der andere Thread die Mutex-Sperre freigibt. Im obigen Beispiel verwenden wir die Funktion pthread_delay_np, um den Thread eine Weile schlafen zu lassen, nur um zu verhindern, dass ein Thread diese Funktion immer belegt. Das obige Beispiel ist sehr einfach und wird hier nicht vorgestellt. Es muss darauf hingewiesen werden, dass bei der Verwendung von Mutex-Sperren sehr wahrscheinlich ein Deadlock auftritt: Zwei Threads versuchen, gleichzeitig zwei Ressourcen zu belegen und sperren die entsprechenden Mutex-Sperren in unterschiedlicher Reihenfolge. Beispielsweise müssen zwei Threads Mutex 1 und Mutex 2 sperren. Thread A sperrt zuerst Mutex 1 und Thread B sperrt zuerst Mutex 2. Zu diesem Zeitpunkt tritt ein Deadlock auf. Zu diesem Zeitpunkt können wir die Funktion pthread_mutex_trylock verwenden, die eine nicht blockierende Version der Funktion pthread_mutex_lock ist. Wenn festgestellt wird, dass ein Deadlock unvermeidlich ist, werden entsprechende Informationen zurückgegeben, und der Programmierer kann eine entsprechende Behandlung für den Deadlock vornehmen. Darüber hinaus handhaben verschiedene Mutex-Sperrtypen Deadlocks unterschiedlich. Das Wichtigste ist jedoch, dass der Programmierer selbst bei der Programmgestaltung auf diesen Punkt achten sollte. 4.3 Bedingte Variablen
Im vorherigen Abschnitt haben wir beschrieben, wie Mutex-Sperren verwendet werden, um Datenfreigabe und Kommunikation zwischen Threads zu implementieren. Ein offensichtlicher Nachteil von Mutex-Sperren besteht darin, dass sie nur zwei Zustände haben: gesperrt und entsperrt. Die Bedingungsvariable gleicht die Mängel der Mutex-Sperre aus, indem sie dem Thread erlaubt, zu blockieren und auf ein Signal eines anderen Threads zu warten. Sie wird häufig zusammen mit der Mutex-Sperre verwendet. Bei Verwendung dient die Bedingungsvariable zum Blockieren eines Threads. Wenn die Bedingung nicht erfüllt ist, entsperrt der Thread häufig den entsprechenden Mutex und wartet auf eine Änderung der Bedingung. Sobald ein anderer Thread die Bedingungsvariable ändert, benachrichtigt er die entsprechende Bedingungsvariable, um einen oder mehrere Threads aufzuwecken, die durch diese Bedingungsvariable blockiert sind. Diese Threads sperren das Mutex erneut und testen erneut, ob die Bedingung erfüllt ist. Im Allgemeinen werden Bedingungsvariablen zum Synchronisieren von Threads verwendet. Die Struktur der Bedingungsvariablen ist pthread_cond_t, und die Funktion pthread_cond_init() wird zum Initialisieren einer Bedingungsvariablen verwendet. Sein Prototyp ist: extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr)); Wobei cond ein Zeiger auf eine pthread_cond_t-Struktur und cond_attr ein Zeiger auf eine pthread_condattr_t-Struktur ist. Die Struktur pthread_condattr_t ist die Attributstruktur der Bedingungsvariable. Wie beim Mutex-Lock können wir damit festlegen, ob die Bedingungsvariable innerhalb des Prozesses oder zwischen Prozessen verfügbar ist. Der Standardwert ist PTHREAD_PROCESS_PRIVATE, was bedeutet, dass diese Bedingungsvariable von jedem Thread im selben Prozess verwendet wird. Beachten Sie, dass initialisierte Bedingungsvariablen nur dann erneut initialisiert oder freigegeben werden können, wenn sie nicht verwendet werden. Die Funktion zum Freigeben einer Bedingungsvariablen ist pthread_cond_destroy (pthread_cond_t cond). Die Funktion pthread_cond_wait() bewirkt, dass ein Thread bei einer Bedingungsvariablen blockiert wird. Sein Funktionsprototyp ist: extern int pthread_cond_wait __P ((pthread_cond_t *__cond, pthread_mutex_t *__mutex)); Der Thread entsperrt die Sperre, auf die Mutex zeigt, und blockiert die Bedingungsvariable cond. Der Thread kann durch die Funktion pthread_cond_signal und die Funktion pthread_cond_broadcast geweckt werden. Es ist jedoch zu beachten, dass die Bedingungsvariable den Thread nur blockiert und aufweckt. Die spezifischen Beurteilungsbedingungen müssen weiterhin vom Benutzer angegeben werden, z. B. ob eine Variable 0 ist usw. Dies können wir aus den folgenden Beispielen ersehen. Nachdem der Thread aufgeweckt wurde, wird erneut geprüft, ob die Beurteilungsbedingung erfüllt ist. Wenn sie nicht erfüllt ist, sollte der Thread im Allgemeinen hier immer noch blockiert sein und auf das nächste Mal warten, bis er aufgeweckt wird. Dieser Vorgang wird im Allgemeinen mit der while-Anweisung implementiert. Eine weitere Funktion zum Blockieren eines Threads ist pthread_cond_timedwait(), deren Prototyp lautet: extern int pthread_cond_timedwait __P ((pthread_cond_t *__cond, pthread_mutex_t *__mutex, __const struct timespec *__abstime)); Es verfügt über einen Zeitparameter mehr als die Funktion pthread_cond_wait(). Nach Ablauf der Abstime-Zeit wird die Blockierung aufgehoben, auch wenn die Bedingungsvariable nicht erfüllt ist. Der Prototyp der Funktion pthread_cond_signal() ist: extern int pthread_cond_signal __P ((pthread_cond_t *__cond)); Es wird verwendet, um einen Thread freizugeben, der aufgrund der Bedingungsvariablen cond blockiert ist. Wenn mehrere Threads aufgrund dieser Bedingungsvariablen blockiert sind, wird durch die Thread-Planungsrichtlinie bestimmt, welcher Thread aktiviert wird. Es ist zu beachten, dass diese Funktion durch eine Mutex-Sperre geschützt werden muss, die die Bedingungsvariable schützt. Andernfalls kann das Signal zur Erfüllung der Bedingung zwischen dem Testen der Bedingung und dem Aufrufen der Funktion pthread_cond_wait gesendet werden, was zu unbegrenzter Wartezeit führt. Unten finden Sie ein einfaches Beispiel für die Verwendung der Funktionen pthread_cond_wait() und pthread_cond_signal().
pthread_mutex_t Anzahl der Sperren;
pthread_cond_t Anzahl_ungleich Null;
vorzeichenlose Anzahl;
Anzahl_dekrementieren () {
pthread_mutex_lock (&count_lock);
während(Anzahl==0)
pthread_cond_wait( &Anzahl_ungleich Null, &Anzahl_Sperre);
Anzahl=Anzahl -1;
pthread_mutex_unlock (&count_lock);
}
Anzahl erhöhen(){
pthread_mutex_lock(&count_lock);
wenn(Anzahl==0)
pthread_cond_signal(&Anzahl_ungleich Null);
Anzahl=Anzahl+1;
pthread_mutex_unlock(&count_lock);
}
Wenn der Zählwert 0 ist, wird die Dekrementfunktion bei pthread_cond_wait blockiert und das Mutex count_lock geöffnet. Wenn zu diesem Zeitpunkt die Funktion increment_count aufgerufen wird, ändert die Funktion pthread_cond_signal() die Bedingungsvariable und weist decrement_count() an, die Blockierung zu beenden. Leser können versuchen, diese beiden Funktionen in zwei Threads separat ausführen zu lassen, um zu sehen, welche Ergebnisse angezeigt werden. Die Funktion pthread_cond_broadcast (pthread_cond_t *cond) wird verwendet, um alle Threads aufzuwecken, die durch die Bedingungsvariable cond blockiert sind. Nachdem diese Threads aktiviert wurden, konkurrieren sie erneut um die entsprechende Mutex-Sperre. Daher muss diese Funktion mit Vorsicht verwendet werden. 4.4 Semaphoren
Ein Semaphor ist im Wesentlichen ein nicht-negativer Ganzzahlzähler, der zur Steuerung des Zugriffs auf eine gemeinsame Ressource verwendet wird. Wenn die gemeinsame Ressource zunimmt, wird die Funktion sem_post() aufgerufen, um das Semaphor zu erhöhen. Die öffentliche Ressource kann nur verwendet werden, wenn der Semaphorwert größer als 0 ist. Nach der Verwendung reduziert die Funktion sem_wait() den Semaphor. Die Funktion sem_trywait() hat dieselbe Funktion wie die Funktion pthread_mutex_trylock() und ist eine nicht blockierende Version der Funktion sem_wait(). Nachfolgend stellen wir nacheinander einige Funktionen im Zusammenhang mit Semaphoren vor. Sie sind alle in der Header-Datei /usr/include/semaphore.h definiert. Der Datentyp des Semaphors ist die Struktur sem_t, die im Wesentlichen eine lange Ganzzahl ist. Die Funktion sem_init() wird zum Initialisieren eines Semaphors verwendet. Sein Prototyp ist: externer int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value)); sem ist ein Zeiger auf die Semaphorstruktur; wenn pshared ungleich 0 ist, wird das Semaphor zwischen Prozessen geteilt, andernfalls kann es nur von allen Threads des aktuellen Prozesses geteilt werden; value gibt den Anfangswert des Semaphors an. Die Funktion sem_post( sem_t *sem ) wird verwendet, um den Wert des Semaphors zu erhöhen. Wenn ein Thread auf diesem Semaphor blockiert ist, wird durch Aufrufen dieser Funktion die Blockierung eines der Threads aufgehoben. Der Auswahlmechanismus wird auch durch die Thread-Planungsrichtlinie bestimmt. Mit der Funktion sem_wait(sem_t*sem) wird der aktuelle Thread blockiert, bis der Wert des Semaphors sem größer als 0 ist. Nachdem die Blockierung aufgehoben wurde, wird der Wert von sem um eins reduziert, was darauf hinweist, dass die öffentliche Ressource nach der Verwendung abgenommen hat. Die Funktion sem_trywait ( sem_t *sem ) ist eine nicht blockierende Version der Funktion sem_wait(), die den Wert des Semaphors sem direkt um eins dekrementiert. Zum Freigeben des Semaphors sem wird die Funktion sem_destroy(sem_t*sem) verwendet. Sehen wir uns ein Beispiel für die Verwendung von Semaphoren an. In diesem Beispiel gibt es insgesamt 4 Threads, von denen zwei für das Lesen der Daten aus der Datei in einen gemeinsamen Puffer verantwortlich sind, und die anderen beiden Threads lesen Daten aus dem Puffer für unterschiedliche Verarbeitungen (Additions- und Multiplikationsoperationen).
/* Datei sem.c */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#MAXSTACK 100 definieren
int Stapel[MAXSTACK][2];
int Größe=0;
sem_t sem;
/* Daten aus Datei 1.dat lesen. Bei jedem Lesen der Daten wird der Semaphor um eins erhöht*/
void LeseDaten1(void){
DATEI *fp=fopen("1.dat","r");
während(!feof(fp)){
fscanf(fp,"%d %d",&stack[Größe][0],&stack[Größe][1]);
sem_post(&sem);
++Größe;
}
fschließen(fp);
}
/*Daten aus Datei 2.dat lesen*/
void LeseDaten2(void){
DATEI *fp=fopen("2.dat","r");
während(!feof(fp)){
fscanf(fp,"%d %d",&stack[Größe][0],&stack[Größe][1]);
sem_post(&sem);
++Größe;
}
fschließen(fp);
}
/*Blockieren und auf Daten im Puffer warten. Nach dem Lesen der Daten den Speicherplatz freigeben und weiter warten*/
void HandleData1(void){
während(1){
sem_wait(&sem);
printf("Plus:%d+%d=%d/n",Stapel[Größe][0],Stapel[Größe][1],
Stapel[Größe][0]+Stapel[Größe][1]);
--Größe;
}
}
void HandleData2(void){
während(1){
sem_wait(&sem);
printf("Multiplizieren:%d*%d=%d/n",Stack[Größe][0],Stack[Größe][1],
Stapel[Größe][0]*Stapel[Größe][1]);
--Größe;
}
}
int Haupt(void){
pthread_t t1, t2, t3, t4;
sem_init(&sem,0,0);
pthread_create(&t1,NULL,(void *)HandleData1,NULL);
pthread_create(&t2,NULL,(void *)HandleData2,NULL);
pthread_create(&t3,NULL,(void *)ReadData1,NULL);
pthread_create(&t4,NULL,(void *)ReadData2,NULL);
/* Verhindern Sie, dass das Programm vorzeitig beendet wird, und lassen Sie es hier unbegrenzt warten */
pthread_join(t1,NULL);
}
Unter Linux verwenden wir den Befehl gcc -lpthread sem.c -o sem, um die ausführbare Datei sem zu generieren. Wir haben die Datendateien 1.dat und 2.dat im Voraus bearbeitet. Unter der Annahme, dass ihr Inhalt 1 2 3 4 5 6 7 8 9 10 bzw. -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 ist, führen wir sem aus und erhalten die folgenden Ergebnisse:
Multiplizieren: -1*-2=2 Plus: -1+-2=-3 Multiplizieren:9*10=90 Plus: -9+-10=-19 Multiplizieren: -7*-8=56 Plus: -5+-6=-11 Multiplizieren: -3*-4=12 Plus:9+10=19 Plus: 7 + 8 = 15 Plus: 5+6=11
Daraus können wir das Wettbewerbsverhältnis zwischen den Threads erkennen. Die Werte werden nicht in der ursprünglich beabsichtigten Reihenfolge angezeigt, da der Größenwert von jedem Thread willkürlich geändert wird. Dies ist auch häufig ein Problem, das bei der Multithread-Programmierung Aufmerksamkeit erfordert. 5 Zusammenfassung
Multithread-Programmierung ist eine sehr interessante und nützliche Technologie. Network Ants, das Multithread-Technologie verwendet, ist eines der am häufigsten verwendeten Download-Tools. Grep, das Multithread-Technologie verwendet, ist um ein Vielfaches schneller als Singlethread-Grep. Es gibt viele ähnliche Beispiele. Ich hoffe, dass jeder die Multithreading-Technologie nutzen kann, um effiziente und praktische Programme zu schreiben. Dies ist das Ende dieses Artikels über die Analyse von Multi-Threaded-Programmbildern unter Linux. Das könnte Sie auch interessieren:- Linux Multithread -Programmierung schneller Start
- Multithread -Programmierung in C -Sprache unter Linux
- Detaillierte Erklärung und einfache Beispiele für Multithreading unter Linux
- Detaillierte Erläuterung von C \ C ++ Multi-Process- und Multi-Thread-Programmierbeispielen unter Linux
- Detaillierte Erläuterung der multitHhread -Programmierung von Linux (nicht auf Linux beschränkt)
- Linux Multithread -Programmierung (v)
- Linux Multithread -Programmierung (IV)
- Multithread -Programmierung unter Linux (Teil 3)
- Linux Multithread -Programmierung (Teil 2)
- Linux Multithread -Programmierung (i)
- Detailliertes Tutorial zur Multithread-Programmierung unter Linux (Threads verwenden Semaphoren, um Kommunikationscode zu implementieren)
|