Heim  >  Artikel  >  php教程  >  Ausführliches Tutorial zur Linux-Multithread-Programmierung (Threads implementieren Kommunikationscode durch Semaphoren)

Ausführliches Tutorial zur Linux-Multithread-Programmierung (Threads implementieren Kommunikationscode durch Semaphoren)

黄舟
黄舟Original
2016-12-13 09:57:131591Durchsuche

Thread-Klassifizierung

Threads können entsprechend ihren Planern in Threads auf Benutzerebene und Threads auf Kernebene unterteilt werden.

(1) Thread auf Benutzerebene
Thread auf Benutzerebene löst hauptsächlich das Problem des Kontextwechsels. Sein Planungsalgorithmus und sein Planungsprozess werden alle vom Benutzer festgelegt, und zur Laufzeit ist keine spezielle Kernel-Unterstützung erforderlich . . Dabei stellt das Betriebssystem häufig eine User-Space-Thread-Bibliothek bereit, die Thread-Erstellung, -Planung, -Abbruch und andere Funktionen bereitstellt, während der Kernel weiterhin nur den Prozess verwaltet. Wenn ein Thread in einem Prozess einen blockierenden Systemaufruf aufruft, wird der Prozess, einschließlich aller anderen Threads im Prozess, ebenfalls blockiert. Der Hauptnachteil dieser Art von Thread auf Benutzerebene besteht darin, dass er die Vorteile von Multiprozessoren bei der Planung mehrerer Threads in einem Prozess nicht nutzen kann.

(2) Thread auf Kernebene
Diese Art von Thread ermöglicht die Planung von Threads in verschiedenen Prozessen nach derselben relativen Prioritätsplanungsmethode, sodass die Parallelitätsvorteile von Multiprozessoren genutzt werden können.
Die meisten Systeme verwenden jetzt die Methode der Koexistenz von Threads auf Benutzerebene und Threads auf Kernebene. Ein Thread auf Benutzerebene kann einem oder mehreren Threads auf Kernebene entsprechen, was einem „Eins-zu-Eins“- oder „Viele-zu-Eins“-Modell entspricht. Dies erfüllt nicht nur die Anforderungen von Multiprozessorsystemen, sondern minimiert auch den Planungsaufwand.

Die Linux-Thread-Implementierung wird außerhalb des Kerns durchgeführt und der Kern stellt die Schnittstelle do_fork() zum Erstellen eines Prozesses bereit. Der Kernel stellt zwei Systemaufrufe clone() und fork() zur Verfügung, die letztendlich die Kernel-API do_fork() mit unterschiedlichen Parametern aufrufen. Wenn Sie Threads implementieren möchten, ist dies natürlich ohne Kernunterstützung für gemeinsam genutzte Datensegmente mit mehreren Prozessen (eigentlich leichtgewichtigen Prozessen) nicht möglich. Daher stellt do_fork() viele Parameter bereit, einschließlich CLONE_VM (gemeinsam genutzter Speicherraum) und CLONE_FS (gemeinsam genutzte Datei). Systeminformationen), CLONE_FILES (gemeinsam genutzte Dateideskriptortabelle), CLONE_SIGHAND (gemeinsam genutzte Signalhandle-Tabelle) und CLONE_PID (gemeinsam genutzte Prozess-ID, nur gültig für den Kernprozess, also Prozess 0). Bei Verwendung des fork-Systemaufrufs ruft der Kernel do_fork() auf, ohne gemeinsame Attribute zu verwenden. Der Prozess verfügt über eine unabhängige Ausführungsumgebung und verwendet Wenn pthread_create () zum Erstellen eines Threads verwendet wird, werden alle diese Attribute schließlich auf den Aufruf von __clone () festgelegt und alle diese Parameter werden an do_fork () im Kern übergeben. Der so erstellte „Prozess“ verfügt nur über eine gemeinsame Betriebsumgebung Der Stapel ist unabhängig und wird von __clone() übergeben.

Linux-Threads existieren in Form von leichtgewichtigen Prozessen innerhalb des Kerns mit unabhängigen Prozesstabelleneinträgen, und alle Erstellungs-, Synchronisierungs-, Lösch- und anderen Vorgänge werden in der pthread-Bibliothek außerhalb des Kerns ausgeführt. pthread Die Bibliothek verwendet einen Verwaltungsthread (__pthread_manager(), jeder Prozess ist unabhängig und einzigartig), um die Erstellung und Beendigung von Threads zu verwalten, Threads Thread-IDs zuzuweisen und Thread-bezogene Signale (z. B. Abbrechen) zu senden, während der Hauptthread ( pthread_create()) Der Aufrufer übergibt die Anforderungsinformationen über die Pipeline an den Verwaltungsthread.

Hauptfunktionsbeschreibung

1. Thread-Erstellung und -Beendigung

pthread_create Thread-Erstellungsfunktion
int pthread_create (pthread_t * thread_id,__const pthread_attr_t * __attr,void *(*__start_routine) (void *),void *__restrict __arg);

Der erste Parameter der Thread-Erstellungsfunktion ist ein Zeiger auf die Thread-ID, 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 zum Ausführen der Funktion. Hier unser Funktionsthread Es sind keine Parameter erforderlich, daher wird der letzte Parameter auf einen Nullzeiger gesetzt. Wir setzen außerdem den zweiten Parameter auf einen Nullzeiger, wodurch ein Thread mit Standardattributen generiert wird. Wenn die Thread-Erstellung erfolgreich ist, gibt die Funktion 0 zurück, wenn nicht 0 Dies bedeutet, dass die Thread-Erstellung fehlgeschlagen ist und der häufige Fehlerrückgabecode EAGAIN ist und EINVAL. Ersteres bedeutet, dass das System beispielsweise die Erstellung neuer Threads einschränkt, 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 Parameter drei und Parameter vier bestimmte Funktion aus, und der ursprüngliche Thread führt weiterhin die nächste Codezeile aus.

pthread_join-Funktion zum Warten auf das Ende eines Threads.
Der Funktionsprototyp ist: int pthread_join (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-blockierende Funktion. Die Funktion, die sie aufruft, wartet, bis der wartende Thread endet. Wenn die Funktion zurückkehrt, werden die Ressourcen des wartenden Threads wiederhergestellt. Ein Thread kann nur von einem Thread beendet werden und sollte sich im verbindungsfähigen Zustand (nicht getrennt) befinden.

pthread_exit Funktion
Es gibt zwei Möglichkeiten, einen Thread zu beenden: Wenn die vom Thread ausgeführte Funktion endet, endet auch der Thread, der sie aufgerufen hat.
Die andere Möglichkeit besteht darin, die Funktion pthread_exit zu verwenden zu erreichen. Sein Funktionsprototyp ist: void pthread_exit (void *__retval) Der einzige Parameter ist der Rückgabecode der Funktion, solange pthread_join Der zweite Parameter thread_return in Nicht NULL, dieser Wert wird an thread_return übergeben. Als letztes ist zu beachten, dass nicht mehrere Threads auf einen Thread warten können. Andernfalls kehrt der erste Thread, der das Signal empfängt, erfolgreich zurück und der Rest ruft pthread_join auf Der Thread gibt den Fehlercode ESRCH zurück.

2. Thread-Eigenschaften

Die Eigenschaften des zweiten Parameterthreads der Funktion pthread_create. Setzen Sie diesen Wert auf NULL, d. h. verwenden Sie die Standardattribute. Viele Attribute des Threads können geändert werden. Zu diesen Eigenschaften gehören hauptsächlich Bindungseigenschaften, Trennungseigenschaften, Stapeladresse, Stapelgröße und Priorität. Die Standardattribute des Systems sind unverbindlich, nicht losgelöst und der Standardwert ist 1M. Stack und die gleiche Prioritätsstufe wie der übergeordnete Prozess. Im Folgenden werden zunächst die Grundkonzepte gebundener Attribute und losgelöster Attribute erläutert.

Bindungsattribut: Linux verwendet einen „Eins-zu-Eins“-Thread-Mechanismus, das heißt, ein Benutzer-Thread entspricht einem Kernel-Thread. Das Bindungsattribut bedeutet, dass ein Benutzerthread fest einem Kernel-Thread zugeordnet ist, da die Planung von CPU-Zeitscheiben für Kernel-Threads erfolgt. (d. h. ein leichter Prozess), sodass Threads mit Bindungsattributen sicherstellen können, dass bei Bedarf immer ein entsprechender Kernel-Thread vorhanden ist. Im Gegensatz dazu bedeutet das nicht bindende Attribut, dass die Beziehung zwischen Benutzer-Threads und Kernel-Threads nicht immer festgelegt ist, sondern vom System gesteuert und zugewiesen wird.

Ablösungsattribut: Das Ablösungsattribut wird verwendet, um zu bestimmen, wie sich ein Thread selbst beendet. Im Falle einer Nichttrennung werden beim Ende eines Threads die von ihm belegten Systemressourcen nicht freigegeben, d. h., es findet keine echte Beendigung statt. Erst wenn die Funktion pthread_join() zurückkehrt, kann der erstellte Thread die von ihm belegten Systemressourcen freigeben. Bei losgelösten Attributen werden die von ihnen belegten Systemressourcen sofort freigegeben, wenn ein Thread endet.
Hier ist Folgendes zu beachten: Wenn Sie das Trennungsattribut eines Threads festlegen und dieser Thread sehr schnell läuft, befindet er sich wahrscheinlich in pthread_create Die Funktion wird beendet, bevor sie zurückkehrt. Nach ihrer Beendigung werden die Thread-Nummer und die Systemressourcen möglicherweise an andere Threads übergeben. Zu diesem Zeitpunkt erhält der Thread, der pthread_create aufruft, die falsche Thread-Nummer.

Bindungsattribute festlegen:

int pthread_attr_init(pthread_attr_t *attr)
int pthread_attr_setscope(pthread_attr_t *attr, int Scope)
int pthread_attr_getscope(pthread_attr_t *tattr, int *scope)
scope: PTHREAD_SCOPE_SYSTEM: Bindung, dieser Thread konkurriert mit allen Threads im System PTHREAD_SCOPE_PROCESS: Ungebunden, dieser Thread konkurriert mit anderen Threads im Prozess

Detach-Attribut festlegen:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
int pthread_attr_getdetachstate(const pthread_attr_t *tattr,int *detachstate)
detachstate PTHREAD_CREATE_DETACHED: PTHREAD trennen _CREATE_JOINABLE: nicht getrennt

Planungsrichtlinie festlegen:

int pthread_attr_setschedpolicy(pthread_attr_t * tattr, int Policy)
int pthread_attr_getschedpolicy(pthread_attr_t * tattr, int *policy)
Richtlinie SCHED_FIFO: First in, first out SCHED_RR: Schleife SCHED_OTHER: Implementierungsdefinierte Methode

Priorität festlegen:

int pthread_attr_setschedparam (pthread_attr_t *attr, struct sched_param *param)
int pthread_attr_getschedparam (pthread_attr_t *attr, struct sched_param *param)

3. Thread-Zugriffskontrolle

1) Mutex-Sperre (Mutex)
erreicht die Synchronisation zwischen Threads über den Sperrmechanismus. Es darf jeweils nur ein Thread einen kritischen Codeabschnitt ausführen.

1 int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
2 int pthread_mutex_lock(pthread_mutex_t *mutex);
3 int pthread_mutex_unlock(pthread_mutex_t *mutex);
4 int pthread_mutex_destroy(pthread_mutex_t *mutex);

(1) Initialisieren Sie zuerst die Sperre init() oder weisen Sie pthread_mutex_t statisch zu mutex=PTHREAD_MUTEX_INITIALIER
(2) Sperren, Sperren, Trylock, Sperrblock wartet auf die Sperre, Trylock gibt EBUSY sofort zurück
(3) Entsperren, Entsperren muss sich im gesperrten Zustand befinden und durch den Sperrthread entsperrt werden
(4) Sperre löschen und zerstören (die Sperre muss zu diesem Zeitpunkt entsperrt werden, sonst wird EBUSY zurückgegeben)

Mutex ist in zwei Typen unterteilt: rekursiv und nicht rekursiv. Dies ist der Name von POSIX . Außerdem ist der Name Reentrant Nicht wiedereintretend. Es gibt keinen Unterschied zwischen diesen beiden Mutexes, wenn sie als Inter-Thread-Synchronisationstools verwendet werden. Ihr einziger Unterschied besteht darin, dass derselbe Thread wiederholt rekursiv mutexieren kann Der Mutex ist gesperrt, aber der nicht rekursive Mutex kann nicht wiederholt gesperrt werden. Sperren.
Nicht rekursiver Mutex wird bevorzugt, definitiv nicht aus Leistungsgründen, sondern um die Designabsicht widerzuspiegeln. nicht rekursiv und rekursiv Der Leistungsunterschied zwischen den beiden ist eigentlich nicht groß, da ein Zähler weniger verwendet wird und ersterer nur etwas schneller ist. Wiederholen Sie den nicht rekursiven Mutex mehrmals im selben Thread Das Sperren führt meiner Meinung nach sofort zu einem Deadlock. Es kann uns helfen, über die Sperranforderungen des Codes nachzudenken und Probleme frühzeitig zu erkennen (während der Codierungsphase). zweifellos rekursiver Mutex Es ist bequemer zu verwenden, da Sie sich keine Sorgen über eine Thread-Sperre selbst machen müssen. Ich denke, das ist der Grund, warum Java und Windows standardmäßig rekursiven Mutex bereitstellen. (Java Die intrinsische Sperre, die mit der Sprache einhergeht, ist reentrant und gleichzeitig Die Bibliothek stellt ReentrantLock bereit, und CRITICAL_SECTION von Windows ist ebenfalls wiedereintrittsfähig. Es scheint, dass keiner von ihnen leichte, nicht rekursive Funktionen bietet Mutex. )

2) Bedingungsvariable (cond)
Ein Mechanismus, der von Threads gemeinsam genutzte globale Variablen zur Synchronisierung verwendet.

1 int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
2 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
3 int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const timespec *abstime);
4 int pthread_cond_destroy(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond);
6 int pthread_cond_broadcast(pthread_cond_t *cond); //Alle Threads entsperren

(1) init() oder pthread_cond_t cond=PTHREAD_COND_INITIALIER; Setzen Sie das Attribut auf NULL
(2) Warten Sie, bis die Bedingung erfüllt ist. pthread_cond_wait,pthread_cond_timedwait.
wait() gibt die Sperre frei und blockiert das Warten darauf, dass die Bedingungsvariable wahr ist.
timedwait() legt die Wartezeit fest, aber es gibt immer noch kein Signal, und gibt ETIMEOUT zurück (die Sperrung stellt sicher, dass nur ein Thread wartet )
(3) Aktivieren Sie die Bedingungsvariable :pthread_cond_signal,pthread_cond_broadcast (aktivieren Sie alle wartenden Threads)
(4) Löschen Sie Bedingungsvariablen:destroy; Es gibt keinen wartenden Thread, andernfalls wird EBUSY

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
zurückgegeben. Diese beiden Funktionen müssen innerhalb des Mutex-Sperrbereichs verwendet werden.

Rufen Sie pthread_cond_signal() auf Beim Freigeben eines bedingt blockierten Threads hat der Aufruf von pthread_cond_signal() keine Auswirkung, wenn kein Thread basierend auf der Bedingungsvariablen blockiert ist. Für Windows, beim Anruf Wenn SetEvent die Bedingung „Auto-Reset-Ereignis“ auslöst und kein Thread durch die Bedingung blockiert wird, funktioniert diese Funktion weiterhin und die Bedingungsvariable befindet sich im ausgelösten Zustand.

Producer-Consumer-Probleme unter Linux (mit Mutexes und Bedingungsvariablen):

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "pthread.h"
#define BUFFER_SIZE 16
struct prodcons  
{  
int buffer[BUFFER_SIZE];  
pthread_mutex_t lock;  //mutex ensuring exclusive access to buffer
int readpos,writepos;  //position for reading and writing
pthread_cond_t notempty;  //signal when buffer is not empty
pthread_cond_t notfull;  //signal when buffer is not full
};  
//initialize a buffer
void init(struct prodcons* b)  
{  
pthread_mutex_init(&b->lock,NULL);  
pthread_cond_init(&b->notempty,NULL);  
pthread_cond_init(&b->notfull,NULL);  
b->readpos = 0;  
b->writepos = 0;  
}  
//store an integer in the buffer
void put(struct prodcons* b, int data)  
{  
pthread_mutex_lock(&b->lock);  
//wait until buffer is not full
while((b->writepos+1)%BUFFER_SIZE == b->readpos)  
{  
printf("wait for not full\n");  
pthread_cond_wait(&b->notfull,&b->lock);  
}
b->buffer[b->writepos] = data;  
b->writepos++;
b->writepos %= BUFFER_SIZE;
pthread_cond_signal(&b->notempty); //signal buffer is not empty
pthread_mutex_unlock(&b->lock);  
}
//read and remove an integer from the buffer
int get(struct prodcons* b)  
{  
int data;  
pthread_mutex_lock(&b->lock);  
//wait until buffer is not empty
while(b->writepos == b->readpos)  
{  
printf("wait for not empty\n");  
pthread_cond_wait(&b->notempty,&b->lock);  
}
data=b->buffer[b->readpos];  
b->readpos++;
b->readpos %= BUFFER_SIZE;
pthread_cond_signal(&b->notfull);  //signal buffer is not full
pthread_mutex_unlock(&b->lock);  
return data;
}
#define OVER -1
struct prodcons buffer;  
void * producer(void * data)  
{  
int n;  
for(n=0; n<50; ++n)  
{
printf("put-->%d\n",n);  
put(&buffer,n);  
}  
put(&buffer,OVER);  
printf("producer stopped\n");  
return NULL;  
}  
void * consumer(void * data)  
{  
int n;  
while(1)  
{  
int d = get(&buffer);  
if(d == OVER) break;  
printf("get-->%d\n",d);  
}
printf("consumer stopped\n");  
return NULL;  
}  
int main()  
{  
pthread_t tha,thb;  
void * retval;  
init(&buffer);  
pthread_creare(&tha,NULL,producer,0);  
pthread_creare(&thb,NULL,consumer,0);  
pthread_join(tha,&retval);  
pthread_join(thb,&retval);  
return 0;  
}
3) Semaphoren

Wie Prozesse können auch Threads Semaphoren verwenden, um die Kommunikation zu implementieren, wenn auch leichtgewichtig.

Die Namen der Semaphorfunktionen beginnen alle mit „sem_“. Es gibt vier grundlegende Semaphorfunktionen, die von Threads verwendet werden.


#include <semaphore.h>
int sem_init(sem_t *sem , int pshared, unsigned int value);
Dies dient dazu, das durch sem angegebene Semaphor zu initialisieren, seine Freigabeoption festzulegen (Linux unterstützt nur 0, was bedeutet, dass es ein lokales Semaphor des aktuellen Prozesses ist) und es dann zu geben Anfangswert VALUE.


Zwei atomare Operationsfunktionen: Beide Funktionen verwenden einen Zeiger auf ein Semaphorobjekt, das durch den sem_init-Aufruf initialisiert wurde, als Parameter.

int sem_wait(sem_t *sem); //给信号量减1,对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。
int sem_post(sem_t *sem); //给信号量的值加1
int sem_destroy(sem_t *sem);
Die Funktion dieser Funktion besteht darin, das Semaphor zu bereinigen, nachdem wir es verwendet haben. Geben Sie alle Ressourcen zurück, die Sie besitzen.

Verwenden Sie Semaphoren, um Produzenten und Konsumenten zu implementieren:

Hier werden vier Semaphoren verwendet, von denen zwei Semaphoren, besetzt und leer, verwendet werden, um die Synchronisation zwischen Produzenten- und Konsumenten-Threads zu lösen besteht darin, dass pmut für den gegenseitigen Ausschluss zwischen mehreren Produzenten und cmut für den gegenseitigen Ausschluss zwischen mehreren Verbrauchern verwendet wird. Unter diesen wird leer auf N (die Anzahl der Raumelemente des begrenzten Puffers) initialisiert, belegt wird auf 0 initialisiert und pmut und cmut werden auf 1 initialisiert.

Referenzcode:

#define BSIZE 64
typedef struct 
{
char buf[BSIZE];
sem_t occupied;
sem_t empty;
int nextin;
int nextout;
sem_t pmut;
sem_t cmut;
}buffer_t;
buffer_t buffer;
void init(buffer_t * b)
{
sem_init(&b->occupied, 0, 0);
sem_init(&b->empty,0, BSIZE);
sem_init(&b->pmut, 0, 1);
sem_init(&b->cmut, 0, 1);
b->nextin = b->nextout = 0;
}
void producer(buffer_t *b, char item) 
{
sem_wait(&b->empty);
sem_wait(&b->pmut);
b->buf[b->nextin] = item;
b->nextin++;
b->nextin %= BSIZE;
sem_post(&b->pmut);
sem_post(&b->occupied);
}
char consumer(buffer_t *b) 
{
char item;
sem_wait(&b->occupied);
sem_wait(&b->cmut);
item = b->buf[b->nextout];
b->nextout++;
b->nextout %= BSIZE;
sem_post(&b->cmut);
sem_post(&b->empty);
return item;
}


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn