Die Verwendung von std::thread

Einführung in die Multithread-Programmierung

Wenn es um Multithread-Programmierung geht, müssen Sie Parallelität und Parallelität erwähnen . Multithreading ist ein Mittel, um Parallelität und Parallelität zu erreichen.

  • Parallelität bedeutet, dass zwei oder mehr unabhängige Operationen gleichzeitig ausgeführt werden .

  • Unter Parallelität versteht man die Ausführung mehrerer Vorgänge innerhalb eines Zeitraums .

Im Single-Core-Zeitalter laufen mehrere Threads gleichzeitig ab und werden nacheinander innerhalb eines bestimmten Zeitraums ausgeführt. Im Multi-Core-Zeitalter können mehrere Threads echte Parallelität und eine wirklich unabhängige parallele Ausführung auf mehreren Kernen erreichen. Beispielsweise können mit den üblichen 4 Kernen und 4 Threads 4 Threads parallel laufen; 4 Kerne und 8 Threads verwenden die Hyper-Threading-Technologie, die einen physischen Kern als 2 logische Kerne simuliert, und können 8 Threads parallel betreiben.

Ansätze zur gleichzeitigen Programmierung

Im Allgemeinen gibt es zwei Möglichkeiten, Parallelität zu erreichen: Multiprozess-Parallelität und Multithread-Parallelität.

Parallelität mehrerer Prozesse

Die Verwendung der Parallelität mehrerer Prozesse besteht darin, eine Anwendung in mehrere unabhängige Prozesse aufzuteilen (jeder Prozess hat nur einen Thread), und diese unabhängigen Prozesse können miteinander kommunizieren, um gemeinsam Aufgaben auszuführen.

Da das Betriebssystem zahlreiche Schutzmechanismen für Prozesse bereitstellt, um zu verhindern, dass ein Prozess die Daten eines anderen Prozesses ändert, ist es einfacher, sicheren Code mit mehreren Prozessen als mit mehreren Threads zu schreiben. Dies führt jedoch auch zu zwei Nachteilen der Multiprozess-Parallelität:

  • Die Kommunikation zwischen Prozessen, unabhängig davon, ob Signale, Sockets, Dateien, Pipes usw. verwendet werden, ist entweder komplizierter oder langsamer oder beides.
  • Das Ausführen mehrerer Prozesse ist teuer und das Betriebssystem stellt viele Ressourcen für die Verwaltung dieser Prozesse bereit.

Wenn mehrere Prozesse gleichzeitig dieselbe Aufgabe ausführen, ist es unvermeidlich, dieselben Daten zu verarbeiten und zwischen Prozessen miteinander zu kommunizieren. Die beiden oben genannten Mängel führen auch dazu, dass die Parallelität mehrerer Prozesse keine gute Wahl ist.

Multithread-Parallelität

Multithread-Parallelität bezieht sich auf die Ausführung mehrerer Threads im selben Prozess.

Diejenigen, die sich mit dem Betriebssystem auskennen, sollten wissen, dass Threads leichte Prozesse sind und jeder Thread unabhängig voneinander verschiedene Befehlssequenzen ausführen kann. Threads besitzen jedoch keine unabhängigen Ressourcen, sondern sind von dem Prozess abhängig, der sie erstellt hat. Das heißt, mehrere Threads im selben Prozess teilen sich denselben Adressraum, können auf die meisten Daten im Prozess zugreifen und Zeiger und Referenzen können zwischen Threads übergeben werden . Auf diese Weise können mehrere Threads im selben Prozess problemlos Daten austauschen und kommunizieren, was für gleichzeitige Vorgänge besser geeignet ist als für Prozesse.

Aufgrund des Mangels an Schutzmechanismen, die das Betriebssystem bereitstellt, müssen Programmierer mehr Arbeit leisten, wenn Multithreads Daten austauschen und kommunizieren, um sicherzustellen, dass Vorgänge an gemeinsam genutzten Datensegmenten in der erwarteten Reihenfolge ausgeführt werden, und versuchen, * zu vermeiden. *Sackgasse**.

std::threadEinführung

Vor C++11 hatten die Windows- und Linux-Plattformen ihre eigenen Multithreading-Standards, und in C++ geschriebenes Multithreading hing oft von bestimmten Plattformen ab.

  • Die Windows-Plattform bietet eine Win32-API für die Erstellung und Verwaltung mehrerer Threads.
  • Unter Linux gibt es den POSIX-Multithreading-Standard, und die von der Threads- oder Pthreads-Bibliothek bereitgestellte API kann unter Unix-ähnlich ausgeführt werden.

Im neuen C++11-Standard kann Multithreading einfach über die Thread-Bibliothek verwaltet werden. Die Thread-Bibliothek kann als Verpackungsschicht für die Multi-Thread-API verschiedener Plattformen betrachtet werden; daher sind Programme, die mit der vom neuen Standard bereitgestellten Thread-Bibliothek geschrieben werden, plattformübergreifend.

Eine einfache Multithread-Implementierung

Die C ++ 11-Standardbibliothek stellt eine Multithread-Bibliothek bereit, #include <thread>für deren Verwendung eine Header-Datei erforderlich ist. Die Header-Datei enthält hauptsächlich Thread-Verwaltungsklassen std::threadund andere mit der Thread-Verwaltung verbundene Klassen. Hier ist ein einfaches Beispiel für die Verwendung der C++-Multithreading-Bibliothek:

#include <iostream>
#include <thread>

using namespace std;

void output(int i)
{
	cout << i << endl;
}

int main()
{
	
	for (uint8_t i = 0; i < 4; i++)
	{
		thread t(output, i);
		t.detach();	
	}
		
	getchar();
	return 0;
}

Erstellen Sie in einer for-Schleife 4 Threads, um die Zahlen 0, 1, 2, 3 auszugeben, und geben Sie am Ende jeder Zahl einen Zeilenumbruch aus. Die Anweisung thread t(output, i)erstellt einen Thread t, der ausgeführt wird output, und der zweite Parameter i ist outputder Parameter, an den übergeben wird t wird nach Abschluss der Erstellung automatisch gestartet, t.detachwas darauf hinweist, dass der Thread im Hintergrund zulässig ist und nicht auf den Abschluss des Threads gewartet werden muss, um die folgenden Anweisungen weiterhin auszuführen. Die Funktion dieses Codes ist sehr einfach. Wenn er nacheinander ausgeführt wird, ist das Ergebnis leicht vorherzusagen:

0
1
2
3

Bei parallelem Multithreading sind die Ergebnisse der Ausführung jedoch unterschiedlich. Das folgende ist beispielsweise das Ergebnis der einmaligen Ausführung des Codes:

01

2
3

Dabei geht es um das Kernthema der Multithread-Programmierung: den Ressourcenwettbewerb .

Unter der Annahme, dass die CPU über 4 Kerne verfügt, können 4 Threads gleichzeitig ausgeführt werden, es gibt jedoch nur eine Konsole und nur ein Thread kann gleichzeitig diese eindeutige Konsole besitzen und die Zahlen ausgeben. Nummerieren Sie die vier Threads, die durch den obigen Code erstellt wurden: t0, t1, t2, t3, und geben Sie die entsprechenden Zahlen aus: 0, 1, 2, 3. Unter Bezugnahme auf die Ausführungsergebnisse in der obigen Abbildung wird der Besitz der Konsole wie folgt übertragen:

  • t0 besitzt die Konsole und gibt die Zahl 0 aus, kommt aber nicht und gibt ein Newline-Zeichen aus, sondern der Besitz des Steuerelements wird auf t1 übertragen; (0)
  • t1 vervollständigt seine eigene Ausgabe, t1-Thread vervollständigt (1\n)
  • Der Konsolenbesitz wird auf t0 übertragen und ein Newline-Zeichen (\n) ausgegeben
  • t2 besitzt die Konsole, vollständige Ausgabe (2\n)
  • t3 besitzt die Konsole, vollständige Ausgabe (3\n)

Da es sich bei der Konsole um eine Systemressource handelt, erfolgt die Verwaltung des Konsolenbesitzes hier durch das Betriebssystem. Wenn jedoch mehrere Threads die Daten im Prozessraum gemeinsam nutzen, müssen Sie Ihren eigenen Code schreiben, um zu steuern, wann jeder Thread die gemeinsam genutzten Daten für den Betrieb besitzen kann.

Die Verwaltung gemeinsam genutzter Daten und die Kommunikation zwischen Threads sind die beiden Kerne der Multithread-Programmierung.

Thread-Management

Jede Anwendung verfügt über mindestens einen Prozess und jeder Prozess verfügt über mindestens einen Hauptthread. Zusätzlich zum Hauptthread können in einem Prozess mehrere Threads erstellt werden. Jeder Thread benötigt eine Einstiegsfunktion. Wenn die Einstiegsfunktion zurückkehrt und beendet wird, wird auch der Thread beendet. Der Hauptthread ist der mainThread mit der Funktion als Einstiegsfunktion.

In der Thread-Bibliothek von C++ 11 wird die Thread-Verwaltung in der Klasse platziert std::thread, und std::threadein Thread kann erstellt und gestartet werden, und der Thread kann angehalten und beendet werden.

einen Thread starten

Die Thread-Bibliothek von C++ 11 ist sehr einfach zum Starten eines Threads. Sie müssen nur ein Objekt erstellen std::thread, ein Thread wird gestartet und das std::threadObjekt wird zum Verwalten des Threads verwendet.

do_task();
std::thread(do_task);

Die übergebene Funktion wird hier erstellt std::thread. Tatsächlich benötigt ihr Konstruktor einen aufrufbaren (aufrufbaren) Typ, solange er über eine Instanz des Funktionsaufruftyps verfügt. Anstatt die Funktion zu übergeben, können Sie Folgendes verwenden:

  • Lambda-Ausdruck

    Verwenden Sie den Lambda-Ausdruck, um den Thread zur Ausgabe von Zahlen zu starten

    for (int i = 0; i < 4; i++)
    {
    	thread t([i]{
    		cout << i << endl;
    	});
    	t.detach();
    }
  • Instanzen von Klassen, die den ()-Operator überladen

    Multithread-Digitalausgabe unter Verwendung einer Klasse, die den ()-Operator überlastet

    class Task
    {
    public:
    	void operator()(int i)
    	{
    		cout << i << endl;
    	}
    };
    
    int main()
    {
    	
    	for (uint8_t i = 0; i < 4; i++)
    	{
    		Task task;
    		thread t(task, i);
    		t.detach();	
    	}
    }

std::threadAchten Sie bei der Übergabe des Funktionsobjekts an den Konstruktor auf einen Fehler beim Parsen der C++-Syntax (der ärgerlichste Parse von C++). Wenn anstelle einer benannten Variablen eine temporäre Variable an std::threadden Konstruktor übergeben wird, tritt ein Syntaxanalysefehler auf. Der folgende Code:

std::thread t(Task());

Dies entspricht der Deklaration einer Funktion t mit dem Rückgabetyp thread, anstatt einen neuen Thread zu starten. Dies kann mit der neuen Initialisierungssyntax vermieden werden

std::thread t{Task()};

Nachdem der Thread gestartet wurde, muss festgelegt werden, wie auf das Ende der Thread-Ausführung gewartet werden soll, bevor die mit dem Thread verbundenethread Zerstörung erfolgt .

C++11 bietet zwei Möglichkeiten, auf das Ende eines Threads zu warten:

  • Im Trennmodus läuft der gestartete Thread unabhängig im Hintergrund und der aktuelle Code wird weiterhin ausgeführt, ohne auf das Ende des neuen Threads zu warten. Dies ist der im vorherigen Code verwendete Ansatz.
    • Der Aufruf von „detach“ bedeutet, dass das Thread-Objekt vollständig von dem Thread getrennt wird, den es darstellt.
    • Der Thread ist nach der Trennung nicht eingeschränkt und kontrolliert und wird separat ausgeführt, bis die Ressource nach der Ausführung freigegeben wird. Dies kann als Daemon-Thread betrachtet werden.
    • Nach der Trennung stellt das Thread-Objekt keinen Thread mehr dar;
    • joinable() == false nach der Trennung, auch wenn es noch ausgeführt wird;
  • Die Join-Methode wartet, bis der gestartete Thread abgeschlossen ist, bevor sie mit der Ausführung fortfährt. Wenn der vorherige Code diese Methode verwendet, lautet seine Ausgabe 0, 1, 2, 3, da jedes Mal, wenn die Ausgabe des vorherigen Threads abgeschlossen ist, der nächste Zyklus ausgeführt und der nächste neue Thread gestartet wird.
    • Nur aktive Threads können Join aufrufen, was mit der Funktion joinable() überprüft werden kann;
    • joinable() == true bedeutet, dass der aktuelle Thread ein aktiver Thread ist, bevor die Join-Funktion aufgerufen wird.
    • Vom Standardkonstruktor erstellte Objekte sind joinable() == false;
    • Join kann nur einmal aufgerufen werden. Danach wird Joinable zu „Falsch“, was darauf hinweist, dass die Thread-Ausführung abgeschlossen ist.
    • Der Thread, der ternimate() aufruft, muss joinable() == false sein;
    • Wenn der Thread die Funktion „join ()“ nicht aufruft, ist er, selbst wenn er ausgeführt wird, immer noch ein aktiver Thread, d. h. „joinable () == true“, und die Funktion „join ()“ kann weiterhin aufgerufen werden.

Rufen Sie in jedem Fall unbedingt or auf, threadbevor Sie es zerstören , um festzustellen, wie der Thread ausgeführt wird.t.joint.detach

Wenn die Join-Methode verwendet wird, wird der aktuelle Code blockiert und die Ausführung wird fortgesetzt, bis der Thread beendet ist.

Die Verwendung der Trennmethode wirkt sich jedoch nicht auf den aktuellen Code aus, der aktuelle Code wird weiterhin nach unten ausgeführt und der erstellte neue Thread wird gleichzeitig ausgeführt. Zu diesem Zeitpunkt muss besonderes Augenmerk auf die Verwendung des erstellten neuen Threads gelegt werden Thread zu den Variablen des aktuellen Bereichs und die Erstellung eines neuen Threads. Nach dem Ende des Thread-Bereichs wird der Thread möglicherweise noch ausgeführt. Zu diesem Zeitpunkt wurden die lokalen Variablen mit der Vervollständigung des Bereichs zerstört. Wenn die Der Thread verwendet weiterhin die Referenz oder den Zeiger der lokalen Variablen . Es tritt ein unerwarteter Fehler auf, der sehr schwerwiegend ist und schwer zu beheben ist. Zum Beispiel:

auto fn = [](const int *a)
{
    for (int i = 0; i < 10; i++)
    {
        cout << *a << endl;
    }
};

[fn]
{
    int a = 1010;
    thread t(fn, &a);
    t.detach();
}();

Verwenden Sie im Lambda-Ausdruck fn, um einen neuen Thread zu starten, verwenden Sie den Zeiger der lokalen Variablen a im neuen Thread und legen Sie den Ausführungsmodus des Threads auf „Trennen“ fest. Auf diese Weise wird die Variable a nach Beendigung der Ausführung des Lamb-Ausdrucks zerstört, der im Hintergrund laufende Thread verwendet jedoch weiterhin den Zeiger der zerstörten Variablen a, was zu falschen Ergebnissen führen kann.

Kopieren Sie daher beim Ausführen eines Threads im Trennmodus die lokalen Daten, auf die der Thread zugreift, in den Thread-Bereich (Wertübergabe) und stellen Sie sicher, dass der Thread keine Referenzen oder Zeiger auf lokale Variablen verwendet, es sei denn, Sie sind sicher, dass dies der Fall ist Der Thread wird in der lokalen Ausführung beendet, bevor der Bereich endet.

Dieses Problem tritt natürlich nicht auf, wenn Sie die Join-Methode verwenden. Dadurch wird der Exit vor dem Ende des Bereichs abgeschlossen.

Warte darauf, dass der Thread in außergewöhnlichem Zustand abgeschlossen wird

Wenn beschlossen wird, den Thread im Trennmodus im Hintergrund laufen zu lassen, kann er threadunmittelbar nach der erstellten Instanz aufgerufen werden detach, sodass der Thread threadvon der nachfolgenden Instanz getrennt wird. Selbst wenn die abnormale threadInstanz zerstört wird, kann der Thread weiterhin ausgeführt werden wird garantiert im Hintergrund ausgeführt.

Wenn der Thread jedoch im Join-Modus ausgeführt wird, muss er joindie Methode an der entsprechenden Position des Hauptthreads aufrufen. Wenn joinvor dem Aufruf eine Ausnahme auftritt, threadwird diese zerstört und der Thread wird durch die Ausnahme beendet. Um zu verhindern, dass der Thread aufgrund einer Ausnahme oder aus bestimmten Gründen, z. B. wenn der Thread auf lokale Variablen zugreift, beendet wird, muss sichergestellt werden, dass der Thread abgeschlossen sein muss, bevor die Funktion beendet wird, und es muss sichergestellt werden, dass die Funktion ausgeführt wird wird aufgerufen, bevor die Funktion beendet wird.join

void func() {
	thread t([]{
		cout << "hello C++ 11" << endl;
	});

	try
	{
		do_something_else();
	}
	catch (...)
	{
		t.join();
		throw;
	}
	t.join();
}

Der obige Code kann garantieren, dass die Methode unter normalen oder abnormalen Bedingungen aufgerufen wird join, sodass der Thread definitiv funcabgeschlossen wird, bevor die Funktion beendet wird. Bei Verwendung dieser Methode ist jedoch nicht nur der Code langwierig, sondern es treten auch einige Umfangsprobleme auf, was keine gute Lösung darstellt.

Eine bessere Methode ist Resource Acquisition Is Initialization (RAII, Resource Acquisition Is Initialization), die eine Klasse bereitstellt und diese im Destruktor aufruft join.

class thread_guard
{
	thread &t;
public :
	explicit thread_guard(thread& _t) :
		t(_t){}

	~thread_guard()
	{
		if (t.joinable())
			t.join();
	}

	thread_guard(const thread_guard&) = delete;
	thread_guard& operator=(const thread_guard&) = delete;
};

void func(){

	thread t([]{
		cout << "Hello thread" <<endl ;
	});

	thread_guard g(t);
}

In jedem Fall ruft die lokale Variable beim Beenden der Funktion gihren Destruktor zum Zerstören auf, sodass joingarantiert werden kann, dass sie aufgerufen wird.

Übergeben Sie Parameter an den Thread

Es ist auch sehr einfach, Parameter an die vom Thread aufgerufene Funktion zu übergeben. Sie müssen threadsie beim Erstellen der Instanz nur der Reihe nach übergeben. Zum Beispiel:

void func(int *a,int n){}

int buffer[10];
thread t(func,buffer,10);
t.join();

Es ist zu beachten, dass die übergebenen Parameter standardmäßig durch Kopieren in den Thread-Bereich kopiert werden, auch wenn der Parametertyp eine Referenz ist. Zum Beispiel:

void func(int a,const string& str);
thread t(func,3,"hello");

funcDer zweite Parameter von ist string &, und es wird ein String-Literal übergeben. Nachdem das Literal const char*als Typ an den Thread-Bereich übergeben wurde, wird es im Thread-Bereich in ** konvertiert string.

Wenn Sie Referenzen verwenden, um Objekte in Threads zu aktualisieren, müssen Sie aufpassen. Standardmäßig wird das Objekt in den Thread-Bereich kopiert. Dies bezieht sich auf das Objekt im kopierten Thread-Bereich und nicht auf das Objekt, das Sie ursprünglich ändern wollten. folgendermaßen:

class _tagNode
{
public:
	int a;
	int b;
};

void func(_tagNode &node)
{
	node.a = 10;
	node.b = 20;
}

void f()
{
	_tagNode node;

	thread t(func, node);
	t.join();

	cout << node.a << endl ;
	cout << node.b << endl ;
}

Im Thread werden die Felder a und b des Objekts auf neue Werte gesetzt, aber nach Beendigung des Thread-Aufrufs ändern sich die Werte dieser beiden Felder nicht. Dies liegt daran, dass die Referenz tatsächlich nodeeine Kopie der lokalen Variablen ist, nicht nodesich selbst. Wenn Sie das Objekt an den Thread übergeben, rufen Sie auf std::refund nodeübergeben Sie die Referenz an den Thread, keine Kopie. Zum Beispiel:thread t(func,std::ref(node));

Sie können Klassenmitgliedsfunktionen auch als Thread-Funktionen verwenden. Beispiele sind wie folgt

class _tagNode{

public:
	void do_some_work(int a);
};
_tagNode node;

thread t(&_tagNode::do_some_work, &node,20);

Der oben erstellte Thread wird aufgerufen node.do_some_work(20), der dritte Parameter ist der erste Parameter der Mitgliedsfunktion und so weiter.

Übertragen Sie den Besitz des Threads

threadEs ist beweglich (beweglich), aber nicht kopierbar (kopierbar). Sie können moveden Eigentümer des Threads ändern, um flexibel zu entscheiden, wann der Thread hinzugefügt oder getrennt wird.

thread t1(f1);
thread t3(move(t1));

Übertragen Sie den Thread von t1 nach t3. Zu diesem Zeitpunkt ist t1 nicht mehr Eigentümer des Threads und beim Aufruf kann eine Ausnahme auftreten t1.join. t1.detachSie müssen t3 verwenden, um den Thread zu verwalten. Dies bedeutet auch, dass threader als Rückgabetyp der Funktion verwendet oder als Parameter an die Funktion übergeben werden kann, wodurch Threads bequemer verwaltet werden können.

Der ID-Typ des Threads ist std::thread::idund es gibt zwei Möglichkeiten, die ID des Threads abzurufen.

  • Rufen Sie es direkt ab , indem Sie threaddie Instanz aufrufenget_id()
  • this_thread::get_id()Rufen Sie Get im aktuellen Thread auf

 

 Autor des Artikels:  Immortalqx

 Artikellink:  http://Immortalqx.github.io/2021/12/04/cpp-notes-3/

 Urheberrechtshinweis:  Sofern nicht anders angegeben, unterliegen alle Artikel in diesem Blog  der CC BY 4.0-  Lizenzvereinbarung. Bitte geben Sie beim Nachdruck die Quelle  Immortalqx an  !

Guess you like

Origin blog.csdn.net/weixin_58045467/article/details/131003378