Einführung in die Multithread-Programmierung

Einführung in die Multithread-Programmierung

1. Einführung in Prozesse und Threads

1.1.Geschichte beider

Im ursprünglichen Betriebssystem gab es kein Konzept für Prozesse und Threads. Sobald eine Aufgabe gestartet wurde, übernahm sie die Kontrolle über die gesamte Maschine. Bevor die Aufgabe abgeschlossen war, gab es keine Möglichkeit, andere Aufgaben auszuführen.
Angenommen, eine Aufgabe A muss während der Ausführung eine große Menge an Dateneingaben (E/A-Vorgänge) lesen. Zu diesem Zeitpunkt kann die CPU nur ruhig warten, bis Aufgabe A das Lesen der Daten abgeschlossen hat, bevor sie mit der Ausführung fortfahren kann, was verschwendet wird. CPU-Ressourcen.

Unterstützt Multitasking-Betriebssysteme: Das Konzept des Multitaskings wurde späteren Betriebssystemen hinzugefügt. Eine Aufgabe bezieht sich auf eine Operation, die von einem oder mehreren Prozessen ausgeführt wird, um einen Zweck zu erreichen. Jeder Prozess entspricht einem bestimmten Speicheradressraum und kann nur seinen eigenen Speicherplatz verwenden, und die Prozesse stören sich nicht gegenseitig. Dadurch besteht die Möglichkeit zur Prozessumschaltung.
Wenn ein Prozess angehalten wird, speichert das Betriebssystem den Status des aktuellen Prozesses (z. B. Prozessidentifikation, vom Prozess verwendete Ressourcen usw.) und stellt ihn beim nächsten Zurückschalten basierend auf den zuvor gespeicherten Daten wieder her Zustand und fahren Sie dann mit der Ausführung fort.
Und die Zuteilung der CPU-Zeit wird vom Betriebssystem verwaltet. Das Betriebssystem weist die Zeit basierend auf dem aktuellen CPU-Status und der Priorität des Prozesses zu und stellt so sicher, dass jedem Prozess die entsprechende CPU-Zeit zugewiesen werden kann.
Anwendungsentwickler müssen keine Energie mehr für die Zuweisung von CPU-Zeit aufwenden, sondern denken stattdessen, dass ihre Programme immer die CPU beanspruchen und sich nur auf das Geschäft konzentrieren, das ihre Anwendungen erreichen wollen. Wenn ein Prozess ausgeführt wird, belegt er tatsächlich nicht immer CPU-Zeit. Das Betriebssystem weist anderen Prozessen CPU-Zeit basierend auf den Prozessattributen zu und belegt die CPU-Zeit des aktuellen Prozesses.

Betriebssystem, das Multithreading unterstützt:
Nach dem Aufkommen von Prozessen wurde die Leistung des Betriebssystems erheblich verbessert, aber die Menschen sind immer noch nicht zufrieden. Die Menschen haben nach und nach Anforderungen an Echtzeitinteraktion. Da ein Prozess jeweils nur eine Aufgabe ausführen kann, kann er, wenn er über mehrere Unteraufgaben verfügt, diese Unteraufgaben nur einzeln ausführen.
Beispielsweise muss ein Überwachungssystem nicht nur Bilddaten auf dem Bildschirm anzeigen, sondern auch mit dem Server kommunizieren, um Bilddaten zu erhalten, und auch die interaktiven Vorgänge von Personen verarbeiten. Wenn das System zu einem bestimmten Zeitpunkt mit dem Server kommuniziert, um Bilddaten abzurufen, und der Benutzer auf eine Schaltfläche im Überwachungssystem klickt, muss das System auf den Erhalt der Bilddaten warten, bevor es den Vorgang des Benutzers verarbeitet. Wenn ja Das Abrufen der Bilddaten dauert 10 Sekunden, dann wartet der Benutzer einfach weiter. Offensichtlich können die Menschen mit einem solchen System nicht zufrieden sein.
Daher wurde das Konzept der Threads eingeführt. Ein Prozess enthält mehrere Threads. Ein Prozess ist die kleinste Einheit der Ressourcenzuweisung und ein Thread ist die kleinste Einheit der CPU-Planung. Threads in einem Prozess können Informationen wie Adressen und Dateideskriptoren innerhalb des Prozesses austauschen. Die CPU-Zeit wird vom Betriebssystem entsprechend den Attributen jedes Threads zugewiesen, wodurch sichergestellt wird, dass jedem Thread die entsprechende CPU-Zeit zugewiesen werden kann. Daher
kann in einem Multithread-System das obige Beispiel des Überwachungssystems mit mehreren Threads implementiert werden . Ein Thread wird verwendet, um Daten vom Server abzurufen, und ein Thread wird verwendet, um auf Benutzerinteraktionen zu reagieren.
Natürlich kann dieses Beispiel im Multiprozessmodus gelöst werden, wobei ein Prozess für die Kommunikation mit dem Server und ein Prozess für die Reaktion auf Benutzervorgänge verwendet wird.

Echtzeitsystem und Nicht-Echtzeitsystem:
Ein Echtzeitsystem bedeutet, dass die Richtigkeit der Berechnung nicht nur von der logischen Richtigkeit des Programms abhängt, sondern auch vom Zeitpunkt der Ergebnisgenerierung Werden die Einschränkungen des Systems nicht erfüllt, tritt ein Systemfehler auf.
Echtzeitplanung unter Allzweck-Linux:
Allzweck-Linux kann Echtzeitplanung erreichen, indem die Priorität von Threads festgelegt wird. Eine ausführliche Beschreibung finden Sie unter: Priorität von Threads. Die
Echtzeitplanung unter Allzweck-Linux verfügt jedoch über die Aufgrund der folgenden Probleme ist ein Echtzeitbetriebssystem erforderlich.
1. Linux-System Die Planungseinheit beträgt 10 ms und kann daher kein genaues Timing liefern.
2. Wenn ein Prozess einen Systemaufruf aufruft, um in den Kernel-Status zu gelangen, kann er nicht vorbelegt werden 3.
In der Linux-Kernel-Implementierung wird eine große Anzahl maskierter Interrupt-Operationen verwendet, was zu einem Interrupt-Verlust führt

Planung unter Echtzeitbetriebssystem:
RTAI (Real-Time Application Interface) ist ein Echtzeitbetriebssystem. Seine Grundidee besteht darin, zur Bereitstellung harter Echtzeitunterstützung in Linux-Systemen ein kleines Echtzeit-Betriebssystem mit einem Mikrokernel zu implementieren (wir nennen es auch das Echtzeit-Subsystem von RT-Linux) und normale Linux-Systeme in dieses umzuwandeln Wird als Aufgabe mit niedriger Priorität im Betriebssystem ausgeführt.
Und die allgemeine Timing-Genauigkeit in Linux-Systemen beträgt 10 ms, das heißt, der Taktzyklus beträgt 10 ms, und RT-Linux kann eine Planungsgranularität auf der Ebene von mehr als einem Dutzend Mikrosekunden bereitstellen, indem die Echtzeituhr des Systems auf einen einzelnen Triggerzustand eingestellt wird.

1.2. Entscheidungen während der Entwicklung

Werfen wir einen Blick auf den Vergleich zwischen Multi-Threading und Multi-Prozess anhand mehrerer unterschiedlicher Dimensionen.
Dimension Multi-Prozess Multi-Thread-Zusammenfassung
Datenfreigabe-Synchronisierung Die Datenfreigabe ist komplex und erfordert IPC; Daten werden getrennt und die Synchronisierung ist einfach, weil Prozessdaten wird gemeinsam genutzt und der Datenaustausch ist einfach. Aus diesem Grund hat die komplexe Synchronisierung jedoch auch ihre eigenen Vorteile. Die
Speicher-
CPU belegt viel Speicher und das Umschalten
ist komplex. Die CPU-Auslastung ist gering. Da weniger Speicher belegt wird, ist das Umschalten einfach. und die CPU-Auslastung ist hoch. Threads dominieren.
Erstellen,
Zerstören
, Wechseln, Erstellen, Zerstören, Wechseln komplex und langsam. Erstellen. Zerstörung und Wechsel sind einfach und die Geschwindigkeit ist hoch. Threads sind dominant. Programmierung: Debuggen und Programmieren
sind
einfach , Debuggen ist einfach, Programmieren ist komplex und Debuggen ist komplex. Prozesse sind dominant.
Zuverlässigkeit. Prozesse beeinflussen sich nicht gegenseitig. Wenn ein Thread aufhängt, bleibt der gesamte Prozess hängen. Prozesse sind dominant.

1. Prioritätsthreads, die große Mengen an Ressourcenfreigabe erfordern.
2. Prioritätsthreads, die häufig erstellt und zerstört werden müssen

2. Ein einfaches Multithreading-Beispiel

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

void* Func(void* pParam);

int main()
{

int iData = 3;

pthread_t ThreadId;
pthread_create(&ThreadId, NULL, Func, &iData);

for(int i=0; i<3; i++)
{
	printf("this is main thread\n");
	sleep(1);
}

pthread_join(ThreadId, NULL);

return 1;

}

void* Func(void* pParam)
{

Fopen

int* pData = (int *)pParam;

for(int i=0; i<*pData; i++)
{
	printf("this is Func thread\n");
	sleep(2);
}
fclose
return NULL;	

}

Kompilieren und ausführen wie folgt:
[root@localhost thread_linuxprj]# g++ -g -o thread_test thread_test.cpp –lpthread

[root@localhost thread_linuxprj]# ./thread_test
Dies ist der Hauptthread.
Dies ist der Func-Thread.
Dies ist der Hauptthread.
Dies ist der Func-Thread.
Dies ist der Hauptthread.
Dies ist der Func-Thread

Dieses Programm verfügt über insgesamt zwei Threads: den Hauptthread und den Thread, der die Funktion Func ausführt. Anhand der Ausgabeergebnisse können Sie erkennen, dass diese beiden Threads gleichzeitig Ausgabevorgänge ausführen.

2.1.Start von Threads

int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);

Parameterbeschreibung:
pthread_t *thread: ID, die dem erstellten Thread entspricht, die eindeutige Kennung des Threads.
const pthread_attr_t *attr: Thread-Attribute, Sie können den Planungsmodus und die Priorität des Threads sowie den DETACH/JOIN-Modus festlegen. Im Allgemeinen auf NULL gesetzt, was darauf hinweist, dass die Standardattribute verwendet werden.
void *( start_routine) (void ): Der vom Thread ausgeführte Funktionszeiger. Diese Funktion muss einen Parameter vom Typ void haben und der Rückgabewert muss vom Typ void sein.
void *arg: Parameter der vom Thread ausgeführten Funktion, die auf NULL gesetzt werden können.

Rückgabewert:
0 zeigt Erfolg an, -1 zeigt Fehler an und Sie können errno verwenden, um den Fehlergrund abzurufen.

2.2. Den Thread stoppen

2.2.1. Thread stoppt automatisch

Nachdem die Ausführung der Thread-Funktion beendet ist, stoppt der Thread automatisch. Im obigen Beispiel stoppt der entsprechende Thread nach der Rückkehr der Fun-Funktion automatisch und sein Rückgabewert kann von anderen Threads abgerufen werden.

Sie können die Funktion pthread_exit auch explizit aufrufen, um den Thread zu stoppen.

Unter normalen Umständen kehren Sie einfach direkt zurück.

2.2.2. Externer Benachrichtigungsthread zum Stoppen

Mit der Funktion pthread_cancel können Sie ein Signal senden, um den Thread zum Stoppen zu benachrichtigen. Nachdem der Thread das Signal empfangen hat, wird er gemäß seinem eigenen Exit-Attribut beendet oder andere Vorgänge ausgeführt.

Es wird im Allgemeinen nicht empfohlen, es zu verwenden, da der Zielthread für diesen Vorgang standardmäßig direkt beendet wird. Dies führt dazu, dass die innerhalb des Threads angewendeten Ressourcen nicht freigegeben werden können, was zu einem Ressourcenverlust führt. Es wird empfohlen, den automatischen Thread-Exit-Modus zu verwenden.

In realen Szenarien ist es jedoch häufig erforderlich, dass ein Thread einen anderen Thread zum Beenden benachrichtigt, wie im folgenden Beispiel:

void* Func(void* pParam)
{ while(true) { Zugehörige Logikverarbeitung durchführen }



int main()
{

pthread_t Thread1;
pthread_create(&Thread1, NULL, Func, NULL);

等待退出信号
请求子线程退出

Der Haupt-Thread erstellt den Sub-Thread Thread1 und wartet dann auf den Ausgang. Wenn ein externes Exit-Signal empfangen wird, wird der Sub-Thread zum Beenden benachrichtigt.

Nachdem der Sub-Thread gestartet wurde, führt er seine eigene logische Verarbeitung weiter aus, bis er vor dem Beenden das Exit-Signal vom Haupt-Thread empfängt.

Da die Verwendung von pthread_cancel nicht empfohlen wird, können Sie das Problem mit dem folgenden Modus lösen

bool g_ThreadExitFlag = false;

void* Func(void* pParam)
{ while(!g_ThreadExitFlag) { Zugehörige Logikverarbeitung durchführen }



	清除本线程所申请的资源
return NULL;

int main()
{ pthread_t Thread1; pthread_create(&Thread1, NULL, Func, NULL);

等待退出信号

g_ThreadExitFlag = true;

pthread_join(Thread1, NULL);

Eine gemeinsam genutzte Variable g_ThreadExitFlag wird zwischen dem Hauptthread und Thread1 verwendet. In der von Thread1 ausgeführten Thread-Funktion bestimmt jede logische Schleife, ob g_ThreadExitFlag wahr ist, löscht die von ihr beantragten Ressourcen und kehrt zurück, und der Thread1-Thread wird automatisch beendet.

2.2.3. Über pthread_join

Nachdem der Hauptthread, der die Man-Funktion ausführt, beendet wird, werden andere Threads in diesem Prozess sofort beendet, unabhängig davon, wo sie ausgeführt werden. Da die Logik des Unterthreads noch nicht abgeschlossen ist, kommt es auf diese Weise zu Situationen, die nicht den Erwartungen entsprechen.
Im obigen Beispiel blockiert der Hauptthread also den Aufruf der Funktion pthread_join, und die Ausgabe nach dem Kompilieren und Ausführen lautet wie folgt:
Dies ist der Hauptthread.
Dies ist der Func-Thread.
Dies ist der Hauptthread.
Dies ist der Func-Thread.
Dies ist der Hauptthread

Laut Code muss Func dreimal gedruckt werden, aber während der tatsächlichen Ausführung wurden alle Threads beendet, da die Man-Funktion vor der Rückkehr von Fun zurückgekehrt ist, sodass Func nur zweimal gedruckt wird.

pthread_join函数原型如下:

int pthread_join(pthread_t thread_id,
void **retval);

pthread_t thread_id: Die Thread-ID, die darauf wartet, Ressourcen wiederzuverwenden.
void **retval: Der Rückgabewert der entsprechenden Thread-Funktion. Wenn Sie nicht aufpassen, können Sie ihn auf NULL setzen.

pthread_join wird verwendet, um auf das Beenden des thread_id-Threads zu warten und die Ressourcen des entsprechenden Threads wiederzuverwenden. Wenn der entsprechende Thread nicht beendet wird, befindet er sich immer im Wartezustand.

Beim Erstellen eines Threads können Sie über den Parameter pthread_attr_t angeben, ob sich der aktuelle Thread im Join-Modus oder im Detach-Modus befindet. Wenn sich der Thread im Join-Modus befindet, müssen Sie die Funktion pthread_join verwenden, um Thread-Ressourcen wiederzuverwenden. Wenn Sie nicht auf den Ausgang des Threads achten müssen, können Sie den Thread in den Trennmodus versetzen. Zu diesem Zeitpunkt müssen Sie die Funktion pthread_join nicht aufrufen, um Thread-Ressourcen wiederzuverwenden. Natürlich kann pthread_join nicht auf den Thread warten Trennmodus.

3. Wettbewerb und Synchronisation zwischen Threads

Beim Entwerfen von Multithread-Programmen lesen und schreiben häufig mehrere Threads gleichzeitig in einen Speicherbereich, was zu Multithread-Konflikten führt und zu einer Inkonsistenz mit den Erwartungen führt. Beispiele unten:

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int g_iData = 0;
void* Func1(void* pParam);
void* Func2(void* pParam);

int main()
{ pthread_t Thread1, Thread2; pthread_create(&Thread1, NULL, Func1, NULL); pthread_create(&Thread2, NULL, Func2, NULL);


pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);

return 1;

}

void* Func1(void* pParam)
{ for (int i=0; i<3; i++) { g_iData = 1;


    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);
}

return NULL;		

}

Zusätzlich zum Hauptthread gibt es zwei Arbeitsthreads, auf denen Func1 bzw. Func2 ausgeführt wird. Es wird erwartet, dass Func1 dreimal „Func1 print 1“ und Fun2 dreimal „Func2 print 2“ druckt.
Die tatsächlichen Ausführungsergebnisse lauten wie folgt:
[root@localhost thread_linuxprj]# ./thread_test
Func2 print g_iData:1
Func1 print g_iData:1 Func1 print g_iData:
1 Func2 print g_iData:1 Func1 print g_iData:2 Func2 print g_iData:2


[root@localhost thread_linuxprj]# ./thread_test
Func2 print g_iData:1
Func1 print g_iData:1
Func1 print g_iData:1
Func2 print g_iData:1
Func1 print g_iData:2
Func2 print g_iData:2

[root@localhost thread_linuxprj]# ./thread_test
Func1 print g_iData:1
Func2 print g_iData:1
Func2 print g_iData:2
Func1 print g_iData:2
Func1 print g_iData:1
Func2 print g_iData:1

Wie aus den Ausführungsergebnissen hervorgeht, werden viele unerwartete Inhalte gedruckt, und der Ausdruck ist bei jeder Ausführung anders.
Dies spiegelt die Eigenschaften von Multithread-Programmen wider, da mehrere Threads Prozessdaten gemeinsam nutzen, die Synchronisierung komplizierter ist und sobald ein Problem auftritt, da die Thread-Planung vom Betriebssystem abhängt und die Reihenfolge der Planung unterschiedlich ist, die externe Leistung des Das Programm ist auch anders und es ist einfach, viele unnötige Probleme zu verursachen, was zu vielen Schwierigkeiten bei der Positionierung führt.

Daher muss beim Programmieren und Codieren versucht werden, eine Multithread-Synchronisation zu entwerfen.

3.1. Wenn es keinen Bedarf für Funktionen gibt, auf dieselben Ressourcen zuzugreifen, versuchen Sie, diese zu vermeiden.

Wenn die vom Thread aufgerufene Funktion nicht auf öffentliche Ressourcen (Speicher, Dateideskriptor) zugreift, liegt überhaupt kein Multithread-Konfliktproblem vor. Greifen Sie daher beim Schreiben von Multithread-Programmen nicht auf dieselben Ressourcen zu, es sei denn, dies ist erforderlich.

Da beispielsweise im obigen Beispiel eines Multithread-Konflikts die beiden Thread-Funktionen auf denselben Speicher zugreifen: die globale Variable g_iData, besteht dieses Problem nicht, wenn es sich bei dem Zugriff nicht um eine globale Variable, sondern um eine Variable in einem eigenen Stapel handelt.
void* Func1(void* pParam)
{ for (int i=0; i<3; i++) { int iData = 1;


    sleep(1);

    printf("Func1 print g_iData:%d\n",  iData);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    int iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n",  iData);
}

return NULL;		

}

Der Grund für die Einführung der Multithread-Programmierung liegt jedoch im Allgemeinen darin, dass mehrere Aufgaben auf dieselben Ressourcen zugreifen müssen, sodass der Zugriff mehrerer Threads auf dieselben Ressourcen unvermeidlich ist. Dies muss mithilfe der folgenden Technologie gelöst werden.

3.2. Mutex-Sperre

Wie im folgenden Code gezeigt, kann mit g_mutex gesteuert werden, dass nur ein Thread gleichzeitig auf dieselbe Ressource zugreift. (Beachten Sie, dass die Verwendung des Sleep-Vorgangs in der Sperre nicht empfohlen wird. Im folgenden Beispiel wird der Sperre nur Sleep hinzugefügt, um Multithread-Konflikte besser zu veranschaulichen.)

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int g_iData = 0;
void* Func1(void* pParam);
void* Func2(void* pParam);

pthread_mutex_t g_mutex;

int main()
{ pthread_mutex_init(&g_mutex, NULL);

pthread_t Thread1, Thread2;
pthread_create(&Thread1, NULL, Func1, NULL);
pthread_create(&Thread2, NULL, Func2, NULL);

pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);

pthread_mutex_destroy(&g_mutex);

return 1;

}

void* Func1(void* pParam)
{ for (int i=0; i<3; i++) { pthread_mutex_lock(&g_mutex);


    g_iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);

    pthread_mutex_unlock(&g_mutex);
}

return NULL;	

}

void* Func2(void* pParam)
{

for (int i=0; i<3; i++)
{
    pthread_mutex_lock(&g_mutex);

    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);

    pthread_mutex_unlock(&g_mutex);
}

return NULL;		

}

Ein bestimmter Thread wird ausgeführt und führt pthread_mutex_lock(&g_mutex) aus. Nach der Besetzung von g_mutex befinden sich andere Threads in einem Wartezustand, wenn sie den Code pthread_mutex_lock(&g_mutex) ausführen. Bis der Thread, der g_mutex belegt, pthread_mutex_unlock (&g_mutex) aufruft, um g_mutex freizugeben, weist das Betriebssystem einen Thread von anderen wartenden Threads zu, der pthread_mutex_lock (&g_mutex) ausführen, die Sperre belegen und die nachfolgende logische Verarbeitung durchführen kann. Threads, die die Sperre nicht belegen, werden fortgesetzt warten. .

Durch die Verwendung eines Mutex wird kein Teil des Speichers gesperrt, sondern eine Operation.

Die Sperre muss vor der Verwendung mit der Funktion pthread_mutex_init initialisiert werden. Wenn die Sperre nicht mehr verwendet wird, muss pthread_mutex_destroy zur Zerstörung aufgerufen werden.

3.2.1. Vorsichtsmaßnahmen

Denken Sie unbedingt daran, nach dem Sperren die Sperre zu entsperren.
Dieses Konzept ist leicht zu verstehen, bei der Verwendung treten jedoch häufig Probleme auf. Wenn Sie eine Sperre verwenden, müssen Sie jede entsperrte Stelle überprüfen, z. B.
void Func()
{ pthread_mutex_lock(&g_mutex) if( …) { pthread_mutex_unlock (&g_mutex) return; } else if(…) { If(…) { pthread_mutex_lock(&g_mutex) Call XXX pthread_mutex_unlock (&g_mutex) return } else { if(…) { If() { //Kein Ausgang hier Entsperren wird einen Deadlock verursachen! Return; } … … pthread_mutex_unlock (&g_mutex) return; } } pthread_mutex_unlock (&g_mutex)






























zurückkehren;
}

pthread_mutex_unlock (&g_mutex)
}

Versuchen Sie, keine Langzeitoperationen in der Sperre auszuführen.
Wenn ein Langzeitalgorithmus im Rahmen einer Sperre ausgeführt wird, führt dies dazu, dass sich andere Threads, die diese Sperre belegen müssen, in einem befinden, sobald ein Thread diese Operation ausführt Langzeitzustand. Der Wartezustand, bis ein langer, zeitaufwändiger Vorgang abgeschlossen ist, verringert die Effizienz des Programms erheblich.

Häufige zeitaufwändige Vorgänge:
Algorithmen, die viel Zeit auf der CPU beanspruchen, Lese- und Schreibvorgänge auf E/A und Ruhezustand.

Wenn in einer bestimmten Schreibsituation mehrere Threads dieselbe E/A lesen und schreiben (z. B. mehrere Threads, die dieselbe Datei ausführen), wird nicht empfohlen, die E/A zu diesem Zeitpunkt zu sperren, sondern das Programmdesign muss nur auf A-Thread geändert werden arbeitet auf IO.

Wenn beispielsweise Thread A und Thread B gleichzeitig eine Datei schreiben möchten, kann diese so geändert werden, dass Thread A und Thread B gleichzeitig in einen Speicherbereich schreiben und zum Aktualisieren ein neuer Thread C hinzugefügt wird die Daten in diesem Speicher in die Datei. Auf diese Weise greift nur ein Thread auf die Datei zu und es ist kein Multithread-Zugriff auf E/A erforderlich. Es muss lediglich der gemeinsam genutzte Speicher gesperrt werden.

Versuchen Sie, keine andere Sperre innerhalb eines Sperrbereichs zu sperren.
Innerhalb des Sperrbereichs von Sperre A sperren Sie Sperre B. Wenn also Sperre A entsperrt werden möchte, muss sie warten, bis der aktuelle Thread die Sperre sperrt. B ist gesperrt erfolgreich, das heißt, Sperre A hängt von Sperre B ab.
Ein solcher Code ist anfällig für zirkuläre Abhängigkeiten. In einem Codeabschnitt hängt Sperre A von Sperre B ab, und in einem anderen Codeabschnitt hängt Sperre B von Sperre A ab. Auf diese Weise kommt es zu einem Deadlock, wenn zwei Threads diese beiden Codeteile ausführen. Zum Beispiel:

void Func1(void* pParam)
{ pthread_mutex_lock(&g_mutexA);

    … …
    pthread_mutex_lock(&g_mutexB);

    … …
		
		pthread_mutex_unlock(&g_mutexB);

   pthread_mutex_unlock(&g_mutexA);

}

void Func2(void* pParam)
{

    pthread_mutex_lock(&g_mutexB);

    … …
    pthread_mutex_lock(&g_mutexA);

    … …
		
		pthread_mutex_unlock(&g_mutexA);

   pthread_mutex_unlock(&g_mutexB);

}

Wenn Thread 1 pthread_mutex_lock(&g_mutexB); in Func1 ausführt, führt Thread 2 pthread_mutex_lock(&g_mutexA); zu diesem Zeitpunkt aus, da Thread 2 die Operation pthread_mutex_lock(&g_mutexB); bereits ausgeführt hat, Thread 1 wartet darauf, g_mutexB zu sperren. Gleichzeitig Da Thread 1 g_mutexA gesperrt hat, befindet sich Thread 2 im Wartezustand für g_mutexA.
Auf diese Weise warten die beiden Threads immer und es kommt zu einem Deadlock.

Der Sperrbereich sollte
so klein wie möglich sein. Je größer der Sperrbereich, desto wahrscheinlicher treten die oben genannten Probleme auf und es ist weniger förderlich für nachfolgende Codeaktualisierungen und Wartungsarbeiten. Daher sollte der Sperrbereich so klein wie möglich sein.

Verwenden Sie die automatische Sperre, um Deadlocks zu vermeiden.
Sie können C++ verwenden, um die Betriebssystem-API zu kapseln und den automatischen Sperrmodus zu implementieren. Während des Lebenszyklus der automatischen Sperre wird der Code automatisch gesperrt, und wenn der Lebenszyklus der automatischen Sperre endet, wird er automatisch entsperrt , wodurch das Problem gelöst wird. Die meisten Deadlock-Probleme. Das obige Beispiel wird mithilfe der automatischen Sperre wie folgt implementiert:

CLock_CS lock_cs;
void Func()
{ AUTO_CRITICAL_SECTION(lock_cs) if(…) { //Der automatische Sperrlebenszyklus endet und wird automatisch entsperrt. return; } else if(…) { If(…) { //Der automatische Sperrlebenszyklus endet und wird automatisch entsperrt. return } else { if(...) { If() { //Der automatische Sperrlebenszyklus endet und wird automatisch entsperrt. Return; } … … //Der automatische Sperrlebenszyklus endet und wird automatisch entsperrt. return; } } //Der automatische Sperrlebenszyklus endet und wird automatisch entsperrt. zurück; }





























//Der automatische Sperrlebenszyklus endet und wird automatisch entsperrt.
}
Informationen zur automatischen Sperrimplementierung finden Sie im folgenden SVN-Code:
https://192.168.20.6:8443/svn/hnc8/trunk/apidev/net/comm/src/criticalsection.h

Verwenden Sie Lese-/Schreibsperren, um die Effizienz zu verbessern.
Anwendungsszenario: Mehrere Threads führen Lese- und Schreibvorgänge aus. Beim Schreiben kann nur einer schreiben, und andere Lese- und Schreibvorgänge befinden sich im Wartezustand. Da beim Lesen keine Änderungen an den öffentlichen Ressourcen vorgenommen werden, können diese gleichzeitig gelesen werden, um die Effizienz zu verbessern.

Wenn jedoch eine Mutex-Sperre verwendet wird, können andere Threads sie nicht belegen, sobald ein Thread die Sperre belegt, und befinden sich im Wartesperrzustand. Daher kann die Forderung nach gleichzeitigem Lesen nicht erfüllt werden.

Dies kann durch Lese-/Schreibsperren erreicht werden:

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

3.3.Bedingte Variablen

Die Verwendung einer Mutex-Sperre kann nur garantieren, dass nur ein Thread gleichzeitig den Code innerhalb des Sperrbereichs ausführen kann, die Ausführungsreihenfolge jedes Threads kann jedoch nicht garantiert werden. Wenn bei der Entwicklung sichergestellt werden muss, dass nachfolgende Threads erst ausgeführt werden können, nachdem ein bestimmter Thread ausgeführt wurde, wird die Verwendung von Bedingungsvariablen empfohlen.
Wie im folgenden Beispiel werden die Threads 1, 2 und 3 gleichzeitig gestartet. Wenn Thread 1 die Ausführung abschließt, wird einer der Threads 2 oder 3 für nachfolgende Vorgänge aktiviert.

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>

int g_iData = 0;
bool g_bNewThreadRun = false;
void* Func1(void* pParam);
void* Func2(void* pParam);
void* Func3(void* pParam);

pthread_mutex_t g_mutex;
pthread_cond_t g_cond;

int main()
{ pthread_mutex_init(&g_mutex, NULL); pthread_cond_init(&g_cond, NULL);

pthread_t Thread1, Thread2, Thread3;
pthread_create(&Thread1, NULL, Func1, NULL);
pthread_create(&Thread2, NULL, Func2, NULL);
 //pthread_create(&Thread3, NULL, Func3, NULL);

pthread_join(Thread1, NULL);
pthread_join(Thread2, NULL);
 pthread_join(Thread3, NULL);

pthread_cond_destroy(&g_cond);
pthread_mutex_destroy(&g_mutex);

return 1;

}

void* Func1(void* pParam)
{

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

    g_iData = 1;

    sleep(1);

    printf("Func1 print g_iData:%d\n", g_iData);   
}

g_bNewThreadRun = true;
 pthread_cond_signal(&g_cond);


return NULL;	

}

void* Func2(void* pParam)
{ pthread_mutex_lock(&g_mutex); while(!g_bNewThreadRun) { pthread_cond_wait(&g_cond, &g_mutex); } g_bNewThreadRun = false; for (int i=0; i<3; i++) { g_iData = 2;








    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);

}

pthread_mutex_unlock(&g_mutex);
return NULL;		

}

void* Func2(void* pParam)
{ pthread_mutex_lock(&g_mutex); while(!g_ bNewThreadRun) { pthread_cond_wait(&g_cond, &g_mutex); }




    g_bNewThreadRun = false;

for (int i=0; i<3; i++)
{
    g_iData = 2;

    sleep(1);

    printf("Func2 print g_iData:%d\n", g_iData);
}

pthread_mutex_unlock(&g_mutex);

return NULL;		

}

Die Threads 1, 2 und 3 werden gestartet. Thread 1 wird sofort ausgeführt, da keine Sperre vorhanden ist, und setzt g_bNewThreadRun = true und löst dann die Bedingungsvariable pthread_cond_signal(&g_cond) aus. Thread 2 führt zuerst den Sperrvorgang pthread_mutex_lock(&g_mutex); aus
. und ruft dann die Funktion pthread_cond_wait(&g_cond, &g_mutex) auf. Wenn Sie diese Funktion eingeben, wird sie zuerst entsperrt und dann darauf gewartet, dass g_cond wirksam wird.
Auf die gleiche Weise führt Thread 3 pthread_mutex_lock(&g_mutex) aus und ruft dann die Funktion pthread_cond_wait(&g_cond, &g_mutex) auf und wartet darauf, dass g_cond wirksam wird.
Wenn Thread 1 die Bedingungsvariable g_cond auslöst, wird einer der Threads 2 oder 3 aktiviert. Die Funktion pthread_cond_wait(&g_cond, &g_mutex) kehrt zurück und g_mutex wird vor der Rückkehr gesperrt, um sicherzustellen, dass nur ein Thread nachfolgende Vorgänge ausführen kann. Wenn die Ausführung abgeschlossen ist, rufen Sie zum Entsperren pthread_mutex_unlock(&g_mutex) auf.

3.3.1. Vorsichtsmaßnahmen

Wenn die Einstellungsbedingung gültig ist, muss sie vor dem Auslösen von pthread_cond_signal vorliegen,
da die Ausführungsreihenfolge der Threads vom Betriebssystem geplant wird. Wenn im obigen Beispiel Thread 1 pthread_cond_signal aufruft und dann g_ bNewThreadRun auf true setzt, ist es möglich, dass die Thread 1 setzt g_ bNewThreadRun nach diesen beiden Schritten auf true. Während dieser Zeit wurde pthread_cond_wait von Thread 2 oder 3 aktiviert. Als Ergebnis wurde festgestellt, dass g_ bNewThreadRun immer noch nicht false war, sodass pthread_cond_wait weiterhin aufgerufen wurde und in war einen Wartezustand, der dazu führt, dass das Gültigkeitssignal der Bedingungsvariablen verloren geht.

Bevor pthread_cond_wait aufgerufen wird, muss die eingehende Sperre gesperrt werden.
Anhand des obigen Beispiels können wir sehen, dass das System nach Eingabe der Funktion pthread_cond_wait zunächst die eingehende Sperre entsperrt. Nachdem es darauf gewartet hat, dass die Bedingungsvariable wirksam wird, sperrt es die eingehende Sperre und kehrt dann zurück. Dies kann nur nachfolgende sicherstellen Ein Thread kann Operationen an öffentlichen Ressourcen ausführen.
Daher muss vor dem Aufruf von pthread_cond_wait die eingehende Sperre gesperrt werden. Wenn keine Sperre vorhanden ist, sind die Ausführungsergebnisse unvorhersehbar.

Wenn Sie pthread_cond_wait aufrufen, müssen Sie zum Schutz die While-Schleifenbeurteilung verwenden,
da die Ausführungsreihenfolge der Threads vom Betriebssystem geplant wird. Im obigen Beispiel hat Thread 1 möglicherweise das Bedingungsvariablensignal ausgelöst, bevor Thread 2 zu pthread_cond_wait ausgeführt wird. Dabei Zeit, Das gültige Signal der Bedingungsvariablen geht verloren und Thread 2 befindet sich immer im Status pthread_cond_wait. Daher muss im Allgemeinen festgestellt werden, ob es im aktuellen Status sofort ausgeführt werden kann, bevor pthread_cond_wait aufgerufen wird.
if(!g_ bNewThreadRun)
{ pthread_cond_wait(&g_cond, &g_mutex); } ... Gleichzeitig kann pthread_cond_wait aufgrund einer Systemunterbrechung zurückkehren und das Bedingungsvariablensignal ist zu diesem Zeitpunkt möglicherweise noch nicht wirksam. Daher müssen Sie eine While-Schleife verwenden, um festzustellen, ob sie im aktuellen Status ausgeführt werden kann. while(!g_ bNewThreadRun) { pthread_cond_wait(&g_cond, &g_mutex); } … … Die Bedingungsvariable wird wiederholt ausgelöst und pthread_cond_wait kann nur einmal abgerufen werden. Gemäß der obigen Erklärung geht das Signal verloren, bevor ein Thread pthread_cond_wait ausführt und ein anderer Thread das Bedingungsvariablensignal auf gültig setzt.











Wenn also Thread 1 pthread_cond_signal mehrmals gleichzeitig auslöst, wird pthread_cond_wait von Thread 2 möglicherweise nur einmal aktiviert, da Thread 2 möglicherweise nachfolgende Vorgänge ausführt und sich nicht im pthread_cond_wait-Zustand befindet, wenn das verbleibende pthread_cond_signal auslöst.
Wenn das Signal einmal ausgelöst werden muss und der Geschäftsverarbeitungsthread einmal ausgeführt werden muss, wird die Verwendung des Semaphormodus empfohlen.

3.4. Signalmenge

Semaphore können verwendet werden, um das typische Producer-Consumer-Muster zu implementieren, wie im folgenden Codebeispiel gezeigt:

#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <errno.h>
#include

mit std::list;

pthread_mutex_t List_Mutex;
Liste g_PdtList;

sem_t g_HavePdtSem;

bool g_bExit = false;

void PutPdt(int iPdt)
{ pthread_mutex_lock(&List_Mutex);

g_PdtList.push_back(iPdt);

pthread_mutex_unlock(&List_Mutex);

}

void GetPdt(int& iPdt)
{ pthread_mutex_lock(&List_Mutex);

iPdt = g_PdtList.front();
g_PdtList.pop_front();

pthread_mutex_unlock(&List_Mutex);

}

void* ProducterFunc(void* pParam);
void* ConsumerFunc(void* pParam);

int main()
{ pthread_mutex_init(&List_Mutex, NULL); sem_init(&g_HavePdtSem, 0, 0);

const int MAX_CONSUMER_NUM = 3;

pthread_t ProductThread;
pthread_t ConsumerThread[MAX_CONSUMER_NUM];

pthread_create(&ProductThread, NULL, ProducterFunc, NULL);

for (int i=0; i<MAX_CONSUMER_NUM; i++)
{
    pthread_create(&ConsumerThread[i], NULL, ConsumerFunc, NULL);
}

sleep(3);
g_bExit = true;


pthread_join(ProductThread, NULL);

for (int i=0; i<MAX_CONSUMER_NUM; i++)
{
    pthread_join(ConsumerThread[i], NULL);
}


sem_destroy(&g_HavePdtSem);
pthread_mutex_destroy(&List_Mutex);

return 1;

}

void* ProducterFunc(void* pParam)
{ for (int i=0; i<5; i++) { printf(“Producter make pdt:%d\n”, i); PutPdt(i);



    sem_post(&g_HavePdtSem);
}

return NULL;	

}

void* ConsumerFunc(void* pParam)
{ static int i = 0; int iIndex = i++;

while(!g_bExit)
{
    timespec abstime;
    clock_gettime(CLOCK_REALTIME, &abstime);

    abstime.tv_sec += 3;
    if (sem_timedwait(&g_HavePdtSem, &abstime) == -1)
    {
        if (errno == ETIMEDOUT)
        {
            continue;
        }
    }

    int iPdt = -1;
    GetPdt(iPdt);

    printf("Consumer[%d] get pdt:%d\n", iIndex, iPdt);
}

return NULL;		

}

编译运行结果如下:
[root@localhost sem_linux_test]# ./thread_test
Producter make pdt:0
Producter make pdt:1
Producter make pdt:2
Consumer[2] get pdt:0
Consumer[0] get pdt:1
Consumer[1] get pdt:2
Producter make pdt:3
Producter make pdt:4
Consumer[0] get pdt:3
Consumer[2] get pdt:4

Wenn die Hauptfunktion startet, ruft sie sem_init(&g_HavePdtSem, 0, 0); auf, um ein Semaphor zu initialisieren.
Anschließend erstellt die Hauptfunktion einen Produktthread und 3 Verbraucherthreads, von denen das Produkt insgesamt 6 Produkte produziert. Jedes Mal, wenn ein Produkt erstellt wird, wird sem_post(&g_HavePdtSem) aufgerufen; der Wert des g_HavePdtSem-Semaphors ist +1.
Nachdem jeder Verbraucherthread erfolgreich erstellt wurde, rufen Sie sem_timedwait (&g_HavePdtSem, &abstime) auf, da der bei der Initialisierung des Semaphors festgelegte Anfangswert 0 (der dritte Parameter von sem_init ist 0) darauf wartet, dass das Semaphor wirksam wird. Nachdem ein gültiges Semaphor erhalten wurde, kehrt sem_timedwait zurück und setzt den Wert des g_HavePdtSem-Semaphors auf -1. bis der Wert des Semaphors auf 0 fällt.

4. Thread-Priorität

线程属性中包含有线程的调度策略和线程的优先级的信息,在创建线程时可以通过线程的属性设置线程的优先级。不过一般情况下多线程设计很少涉及到线程优先级的修改。
当线程属性中调度策略有如下三个类型。
	SCHED_FIFO:实时调度,先进先出,线程启动后一直占用CPU运行,一直到有比此线程优先级更高的线程处于就绪态才释放CPU
	SCHED_RR:实时调度,时间片轮转算法,当线程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。
	SCHED_OTHER:分时调度,默认算法。

Die festgelegte Thread-Priorität ist nur wirksam, wenn die Planungsrichtlinie des Threads SCHED_FIFO und SCHED_RR ist.

Mit der folgenden Funktion können Sie den Planungsalgorithmus und die Priorität des Threads ändern
int pthread_attr_setschedparam(pthread_attr_t *attr,
const struct sched_param *param);

int pthread_attr_setschedpolicy(pthread_attr_t *attr,
int Policy);

5.Der Unterschied zwischen Linux und Win

Das Obige sind alle Thread-Operationen unter Linux. Die Thread-Operations-API unter Win unterscheidet sich von Linux.
Thread startet und wartet auf Exit
uintptr_t _beginthreadex( void *security,
unsigned stack_size,
unsigned (*start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr );

DWORD WINAPI WaitForSingleObject(HANDLE hHandle,
DWORD dwMilliseconds)

互斥锁
HANDLE WINAPI CreateMutex(
__in LPSECURITY_ATTRIBUTES lpMutexAttributes,
__in BOOL bInitialOwner,
__in LPCTSTR lpName
);

BOOL WINAPI ReleaseMutex(
__in HANDLE hMutex
);

In der Win-Umgebung wird im Allgemeinen empfohlen, kritische Abschnitte anstelle von Sperren zu verwenden. Die Aufrufeffizienz kritischer Abschnitte ist viel höher als die von Sperren.

临界区
void WINAPI InitializeCriticalSection(
__out LPCRITICAL_SECTION lpCriticalSection
);
void WINAPI DeleteCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

void WINAPI EnterCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

void WINAPI LeaveCriticalSection(
__in_out LPCRITICAL_SECTION lpCriticalSection
);

Synchronisationsereignis
BOOL WINAPI SetEvent(
__in HANDLE hEvent
);

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

信号量
BOOL WINAPI ReleaseSemaphore(
__in HANDLE hSemaphore,
__in LONG lReleaseCount,
__out LPLONG lpPreviousCount
);
DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

6. Verfügbare Codebasen

Da die APIs für Multithread-Vorgänge unter Windows und Linux unterschiedlich sind, ist es bei Verwendung in Code, der sowohl Win als auch Linux unterstützt, häufig erforderlich, überall Kompilierungsmakros hinzuzufügen, was der Codewartung nicht förderlich ist.
Daher können Sie den gekapselten Code und die dynamische Bibliothek (offener Quellcode, keine Urheberrechtsprobleme) verwenden, um die Verarbeitung auf der oberen Ebene zu erleichtern, ohne Kompilierungsmakros hinzufügen zu müssen.

Ich denke du magst

Origin blog.csdn.net/p309654858/article/details/132145206
Empfohlen
Rangfolge