Multithreading bei C und Win32

Der Microsoft C/C++-Compiler (MSVC) bietet Unterstützung für das Erstellen von Multithreadanwendungen. Erwägen Sie die Verwendung mehrerer Threads, wenn Ihre Anwendung teure Vorgänge ausführen muss, die dazu führen würden, dass die Benutzeroberfläche nicht mehr reagiert.

Bei MSVC gibt es mehrere Möglichkeiten zum Programmieren mit mehreren Threads: Sie können C++/WinRT und die Windows-Runtime-Bibliothek, die Microsoft Foundation Class (MFC)-Bibliothek, C++/CLI und die .NET-Laufzeit oder die C-Laufzeitbibliothek und die Win32-API verwenden. In diesem Artikel geht es um Multithreading in C. Beispielcode finden Sie unter Beispiel-Multithreadprogramm in C.

Multithreadprogramme

Ein Thread ist im Grunde ein Pfad der Ausführung durch ein Programm. Es ist auch die kleinste Ausführungseinheit, die Win32 plant. Ein Thread besteht aus einem Stapel, dem Status der CPU-Register und einem Eintrag in der Ausführungsliste des Systemplanrs. Jeder Thread teilt alle Ressourcen des Prozesses.

Ein Prozess besteht aus einem oder mehreren Threads und dem Code, den Daten und anderen Ressourcen eines Programms im Arbeitsspeicher. Typische Programmressourcen sind offene Dateien, Semaphoren und dynamisch zugeordneten Arbeitsspeicher. Ein Programm wird ausgeführt, wenn der Systemplaner einen seiner Threads Ausführungskontrolle gibt. Der Scheduler bestimmt, welche Threads ausgeführt werden sollen und wann sie ausgeführt werden sollen. Threads mit niedrigerer Priorität müssen möglicherweise warten, während Threads mit höherer Priorität ihre Aufgaben ausführen. Auf Multiprozessorcomputern kann der Scheduler einzelne Threads auf verschiedene Prozessoren verschieben, um die CPU-Auslastung auszugleichen.

Jeder Thread in einem Prozess funktioniert unabhängig. Sofern Sie sie nicht gegenseitig sichtbar machen, werden die Threads einzeln ausgeführt und kennen die anderen Threads in einem Prozess nicht. Threads, die gemeinsame Ressourcen gemeinsam nutzen, müssen ihre Arbeit jedoch mithilfe von Semaphoren oder einer anderen Methode der Interprozesskommunikation koordinieren. Weitere Informationen zum Synchronisieren von Threads finden Sie unter Schreiben eines Multithread-Win32-Programms.

Bibliotheksunterstützung für Multithreading

Alle Versionen des CRT unterstützen jetzt Multithreading, mit Ausnahme der nicht sperrenden Versionen einiger Funktionen. Weitere Informationen finden Sie unter Leistung von Multithread-Bibliotheken. Informationen zu den Versionen des CRT, die mit Ihrem Code verknüpft werden können, finden Sie unter CRT-Bibliotheksfeatures.

Includedateien für Multithreading

Standard CRT enthalten Dateien, die die C-Laufzeitbibliotheksfunktionen deklarieren, während sie in den Bibliotheken implementiert werden. Wenn Die Compileroptionen die konventionen für aufrufe __fastcall oder __vectorcall angeben, geht der Compiler davon aus, dass alle Funktionen mithilfe der Registeraufrufkonvention aufgerufen werden sollen. Die Laufzeitbibliotheksfunktionen verwenden die C-Aufrufkonvention, und die Deklarationen im Standard enthalten Dateien weisen den Compiler an, korrekte externe Verweise auf diese Funktionen zu generieren.

CRT-Funktionen für Threadsteuerung

Alle Win32-Programme weisen mindestens einen Thread auf. Jeder Thread kann zusätzliche Threads generieren. Ein Thread kann seine Aufgabe schnell abschließen oder während der gesamten Lebensdauer eines Programms aktiv bleiben.

Die CRT-Bibliotheken bieten die folgenden Funktionen für die Threaderstellung und -beendigung: _beginthread, _beginthreadex, _endthread und _endthreadex.

Mit der _beginthread-Funktion und der _beginthreadex-Funktion wird ein neuer Thread erstellt; wenn der Vorgang erfolgreich war, wird ein Threadbezeichner zurückgegeben. Der Thread wird automatisch beendet, wenn er die Ausführung abgeschlossen hat. Oder es kann sich mit einem Anruf _endthread oder _endthreadex.

Hinweis

Wenn Sie C-Laufzeitroutinen aus einem Programm aufrufen, das mit libcmt.lib erstellt wurde, müssen Sie Ihre Threads mit der oder _beginthreadex der _beginthread Funktion starten. Verwenden Sie nicht die Win32-Funktionen ExitThread und CreateThread. Bei der Verwendung von SuspendThread tritt möglicherweise ein Deadlock auf, wenn mehrere blockierte Threads darauf warten, dass der unterbrochene Thread den Zugriffsvorgang auf eine C-Laufzeitdatenstruktur abschließt.

Die Funktionen _beginthread und _beginthreadex

Die _beginthread-Funktion und die _beginthreadex-Funktion erstellen einen neuen Thread. Ein Thread nutzt den Code und die Datensegmente eines Prozesses gemeinsam mit anderen Threads in diesem Prozess, verfügt jedoch über eigene, eindeutige Registerwerte, Stapelspeicher und eine aktuelle Anweisungsadresse. Das System weist jedem Thread seine bestimmte CPU-Zeit zu, damit alle Threads in einem Prozess gleichzeitig ausgeführt werden können.

_beginthread und _beginthreadex ähneln der CreateThread-Funktion in der Win32-API, weisen jedoch die folgenden Unterschiede auf:

  • Sie initialisieren bestimmte Variablen der C-Laufzeitbibliothek. Dies ist nur dann wichtig, wenn Sie die C-Laufzeitbibliothek in Ihren Threads verwenden.

  • Mit CreateThread können Sicherheitsattribute gesteuert werden. Mit dieser Funktion können Sie einen Thread starten, der sich im angehaltenen Status befindet.

_beginthread und _beginthreadex geben ein Handle für den neuen Thread zurück, wenn der Vorgang erfolgreich war. Bei einem Fehler wird ein Fehlercode zurückgegeben.

Die funktionen _endthread und _endthreadex

Die _endthread-Funktion beendet einen von _beginthread (und ähnlichen) _endthreadex erstellten Thread, der einen von _beginthreadex) erstellten Thread beendet. Threads werden automatisch beendet, wenn sie abgeschlossen sind. _endthread und _endthreadex sind zum bedingten Beenden aus einem Thread heraus sinnvoll. Ein für die Kommunikationsverarbeitung verantwortlicher Thread kann z. B. seine Aktivität einstellen, wenn die Steuerung des Kommunikationsanschlusses derzeit nicht möglich ist.

Schreiben von Win32-Multithreadprogrammen

Wenn Sie ein Programm mit mehreren Threads schreiben, müssen Sie das Verhalten und die Verwendung der Programmressourcen koordinieren. Stellen Sie außerdem sicher, dass jeder Thread einen eigenen Stapel erhält.

Gemeinsame Ressourcen zwischen Threads freigeben

Hinweis

Eine ähnliche Diskussion aus MFC-Sicht finden Sie unter Multithreading: Programmieren Tipps und Multithreading: Wann die Synchronisierungsklassen verwendet werden sollen.

Jeder Thread verfügt über einen eigenen Stapel und eine eigene Kopie der CPU-Register. Andere Ressourcen, z. B. Dateien, statische Daten und Heapspeicher, werden von allen Threads im Prozess gemeinsam genutzt. Threads, die diese gemeinsamen Ressourcen verwenden, müssen synchronisiert werden. Win32 bietet verschiedene Möglichkeiten zur Synchronisierung von Ressourcen, einschließlich Semaphore, kritischen Abschnitten, Ereignissen und Mutexen.

Wenn mehrere Threads auf statische Daten zugreifen, muss das Programm mögliche Ressourcenkonflikte verarbeiten können. Erwägen Sie ein Programm, bei dem ein Thread eine statische Datenstruktur aktualisiert, die x,y-Koordinaten enthält, damit Elemente von einem anderen Thread angezeigt werden. Wenn der Aktualisierungsthread die x-Koordinate ändert und vor dem Ändern der Y-Koordinate vorab eingestellt wird, wird der Anzeigethread möglicherweise geplant, bevor die Y-Koordinate aktualisiert wird. In diesem Fall wird das Element an der falschen Stelle angezeigt. Durch die Verwendung von Semaphore zur Steuerung des Zugriffs auf die Struktur können Sie dieses Problem vermeiden.

Ein Mutex (kurz für mutuale Exklusion) ist eine Möglichkeit, zwischen Threads oder Prozessen zu kommunizieren, die asynchron voneinander ausgeführt werden. Diese Kommunikation kann verwendet werden, um die Aktivitäten mehrerer Threads oder Prozesse zu koordinieren, in der Regel durch Steuern des Zugriffs auf eine freigegebene Ressource durch Sperren und Entsperren der Ressource. Um dieses X-Y-Koordinatenupdateproblem zu beheben, legt der Updatethread einen Mutex fest, der angibt, dass die Datenstruktur vor dem Ausführen der Aktualisierung verwendet wird. Nach Verarbeitung der beiden Koordinaten wird der Mutex aufgehoben. Der Anzeigethread muss abwarten, bis der Mutex aufgehoben ist, bevor er die Anzeige aktualisieren kann. Dieser Prozess des Wartens auf einen Mutex wird häufig als Blockierung für einen Mutex bezeichnet, da der Prozess blockiert wird und erst fortgesetzt wird, wenn der Mutex gelöscht wird.

Das Im Beispiel-Multithread-C-Programm gezeigte Bounce.c-Programm verwendet einen Mutex, der zum Koordinieren von Bildschirmaktualisierungen benannt ScreenMutex ist. Jedes Mal, wenn eines der Anzeigethreads zum Schreiben auf den Bildschirm bereit ist, ruft WaitForSingleObject es mit dem Handle an ScreenMutex und konstant INFINITE auf, um anzugeben, dass der WaitForSingleObject Aufruf auf dem Mutex blockiert werden soll und kein Timeout. Wenn ScreenMutex klar ist, legt die Wartefunktion den Mutex fest, sodass andere Threads die Anzeige nicht beeinträchtigen können und die Ausführung des Threads fortgesetzt wird. Ansonsten wird der Thread solange blockiert, bis der Mutex aufgehoben wird. Wenn der Thread das Anzeigeupdate abgeschlossen hat, wird der Mutex durch Aufrufen ReleaseMutexfreigegeben.

Bildschirmaktualisierungen und statische Daten sind nur zwei der Ressourcen, bei deren Verwaltung mit Bedacht vorgegangen werden muss. Ein Programm verfügt möglicherweise über mehrere Threads, die auf dieselbe Datei zugreifen. Da durch einen anderen Thread möglicherweise der Dateizeiger verschoben wurde, muss von jedem Thread vor dem Lese- oder Schreibvorgang der Dateizeiger zurückgesetzt werden. Darüber hinaus muss jeder Thread sicherstellen, dass er nicht zwischen dem Zeitpunkt, zu dem er den Zeiger positioniert, und dem Zeitpunkt, zu dem er auf die Datei zugreift, nicht vorgedrungen ist. Diese Threads sollten einen Semaphor verwenden, um den Zugriff auf die Datei zu koordinieren, indem jeder Dateizugriff mit WaitForSingleObject und ReleaseMutex Aufrufen in eckige Klammern gesetzt wird. Diese Vorgehensweise wird anhand des folgenden Codefragments erläutert:

HANDLE    hIOMutex = CreateMutex (NULL, FALSE, NULL);

WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

Threadstapel

Der gesamte standardmäßige Stapelspeicher einer Anwendung ist für den ersten Ausführungsthread belegt, der als Thread 1 bezeichnet wird. Folglich müssen Sie für jeden von einem Programm zusätzlich benötigten Thread angeben, wie viel mehr Speicherplatz für einen separaten Stapel belegt werden soll. Das Betriebssystem ordnet dem Thread bei Bedarf zusätzlichen Stapelspeicher zu, Sie müssen jedoch einen Standardwert angeben.

Das erste Argument im _beginthread Aufruf ist ein Zeiger auf die BounceProc Funktion, die die Threads ausführt. Das zweite Argument legt die Standardgröße des Stapel für den Thread fest. Das letzte Argument ist eine ID-Nummer, die an .BounceProc BounceProc verwendet die ID-Nummer, um den Zufallszahlengenerator zu seedieren und das Farbattribut und das Anzeigezeichen des Threads auszuwählen.

Threads, die Aufrufe an die C-Laufzeitbibliothek oder die Win32-API richten, müssen über ausreichend Stapelspeicher für die Bibliotheks- und API-Funktionen verfügen, die von ihnen aufgerufen werden. Für die C-Funktion printf sind mehr als 500 Bytes Stapelspeicher erforderlich, und Beim Aufrufen von Win32-API-Routinen sollten 2 KB Stapelspeicher verfügbar sein.

Da jeder Thread über einen eigenen Stapel verfügt, können Sie potenzielle Konflikte in Bezug auf Datenelemente vermeiden, indem Sie so wenig statische Daten wie möglich verwenden. Achten Sie bei der Entwicklung eines Programms darauf, dass es automatische Stapelvariablen für alle Daten verwendet, die einem Thread zugehörig sein können. Die einzigen globalen Variablen im Bounce.c-Programm sind mutexes oder Variablen, die sich nach der Initialisierung nie ändern.

Win32 stellt auch Thread-local storage (TLS) zum Speichern von Threaddaten bereit. Weitere Informationen finden Sie unter Thread local storage (TLS).For more information, see Thread local storage (TLS).

Vermeiden von Problembereichen bei Multithreadprogrammen

Es gibt mehrere Probleme, die beim Erstellen, Verknüpfen oder Ausführen eines Multithread-C-Programms auftreten können. Einige der häufigeren Probleme werden in der folgenden Tabelle beschrieben. (Eine ähnliche Diskussion aus MFC-Sicht finden Sie unter Multithreading: Programmieren Tipps.)

Problem Wahrscheinliche Ursache
Sie erhalten ein Meldungsfeld, in dem angezeigt wird, dass Ihr Programm einen Schutzverletzung verursacht hat. Viele Win32-Programmierfehler verursachen Schutzverletzungen. Eine häufige Ursache von Schutzverletzungen ist die indirekte Zuweisung von Daten zu Nullzeigern. Da es dazu führt, dass Ihr Programm versucht, auf den Arbeitsspeicher zuzugreifen, der nicht dazu gehört, wird eine Schutzverletzung ausgegeben.

Eine einfache Möglichkeit, die Ursache einer Schutzverletzung zu erkennen, besteht darin, Das Programm mit Debuginformationen zu kompilieren und dann über den Debugger in der Visual Studio-Umgebung auszuführen. Wenn der Schutzfehler auftritt, überträgt Windows die Steuerung an den Debugger, und der Cursor wird in der Zeile positioniert, die das Problem verursacht hat.
Ihr Programm generiert zahlreiche Kompilierungs- und Verknüpfungsfehler. Sie können viele potenzielle Probleme beseitigen, indem Sie die Warnstufe des Compilers auf einen der höchsten Werte festlegen und die Warnmeldungen abgleichen. Mithilfe der Optionen der Warnungsstufe 3 oder Ebene 4 können Sie unbeabsichtigte Datenkonvertierungen, fehlende Funktionsprototypen und die Verwendung von Nicht-ANSI-Features erkennen.

Siehe auch

Multithreadingunterstützung für älteren Code (Visual C++)
Beispiel-Multithreadprogramm in C
Lokaler Threadspeicher (TLS)
Parallelität und asynchrone Vorgänge mit C++/WinRT
Multithreading mit C++ und MFC