Delen via


Multithreading met C en Win32

De Microsoft C/C++-compiler (MSVC) biedt ondersteuning voor het maken van multithread-toepassingen. Overweeg om meer dan één thread te gebruiken als uw toepassing dure bewerkingen moet uitvoeren waardoor de gebruikersinterface niet meer reageert.

Met MSVC zijn er verschillende manieren om te programmeren met meerdere threads: u kunt C++/WinRT en de Windows Runtime-bibliotheek, de Microsoft Foundation Class-bibliotheek (MFC), C++/CLI en de .NET-runtime of de C-runtime en de Win32-API gebruiken. Dit artikel gaat over multithreading in C. Zie Voorbeeld van een multithread-programma in C.

Multithread-programma's

Een thread is in feite een uitvoeringspad binnen een programma. Het is ook de kleinste uitvoeringseenheid die Win32 plant. Een thread bestaat uit een stack, de status van de CPU-registers en een vermelding in de uitvoeringslijst van de systeemplanner. Elke thread deelt alle resources van het proces.

Een proces bestaat uit een of meer threads en de code, gegevens en andere resources van een programma in het geheugen. Typische programmaresources zijn geopende bestanden, semaphores en dynamisch toegewezen geheugen. Een programma wordt uitgevoerd wanneer de systeemplanner een van zijn threads uitvoeringscontrole geeft. De scheduler bepaalt welke threads moeten worden uitgevoerd en wanneer ze moeten worden uitgevoerd. Threads met een lagere prioriteit moeten mogelijk wachten terwijl threads met een hogere prioriteit hun taken voltooien. Op multiprocessorcomputers kan de scheduler afzonderlijke threads naar verschillende processors verplaatsen om de CPU-belasting te verdelen.

Elke thread in een proces werkt onafhankelijk. Tenzij u ze zichtbaar voor elkaar maakt, worden de threads afzonderlijk uitgevoerd en zijn ze zich niet bewust van de andere threads binnen hetzelfde proces. Threads die algemene resources delen, moeten hun werk echter coördineren met behulp van semaforen of een andere methode voor communicatie tussen processen. Zie Een Multithreaded Win32-programma schrijven voor meer informatie over het synchroniseren van threads.

Bibliotheekondersteuning voor multithreading

Alle versies van de CRT ondersteunen nu multithreading, met uitzondering van de niet-vergrendelingsversies van sommige functies. Zie de prestaties van multithreaded bibliotheken voor meer informatie. Zie crt-bibliotheekfuncties voor informatie over de versies van de CRT die beschikbaar zijn om te koppelen aan uw code.

Bestanden opnemen voor multithreading

Standaard CRT bevat bestanden die de C-runtimebibliotheekfuncties declareren wanneer ze in de bibliotheken worden geïmplementeerd. Als uw compileropties de __fastcall of __vectorcall aanroepende conventies opgeven, gaat de compiler ervan uit dat alle functies moeten worden aangeroepen met behulp van de registratieconventie voor aanroepen. De runtimebibliotheekfuncties maken gebruik van de C-aanroepconventie en de declaraties in de standaardversie bevatten bestanden die de compiler vertellen de juiste externe verwijzingen naar deze functies te genereren.

CRT-functies voor threadbeheer

Alle Win32-programma's hebben ten minste één thread. Elke thread kan aanvullende threads maken. Een thread kan het werk snel voltooien en vervolgens beëindigen, of het kan actief blijven voor het leven van het programma.

De CRT-bibliotheken bieden de volgende functies voor het maken en beëindigen van threads: _beginthread, _beginthreadex, _endthread en _endthreadex.

De _beginthread en _beginthreadex functies maken een nieuwe thread en retourneren een thread-id als de bewerking is geslaagd. De thread wordt automatisch beëindigd als de uitvoering is voltooid. Of het kan zichzelf beëindigen met een aanroep naar _endthread of _endthreadex.

Opmerking

Als u C-runtimeroutines aanroept vanuit een programma dat is gebouwd met libcmt.lib, moet u uw threads starten met de _beginthread of _beginthreadex functie. Gebruik de Win32-functies ExitThread en CreateThread niet. Het gebruik SuspendThread kan leiden tot een impasse wanneer meer dan één thread wordt geblokkeerd totdat de onderbroken thread de toegang tot een C-runtimegegevensstructuur heeft voltooid.

De functies _beginthread en _beginthreadex

De _beginthread en _beginthreadex functies maken een nieuwe thread. Een thread deelt de code en gegevenssegmenten van een proces met andere threads in het proces, maar heeft zijn eigen unieke registerwaarden, stackruimte en huidig instructieadres. Het systeem geeft CPU-tijd aan elke thread, zodat alle threads in een proces gelijktijdig kunnen worden uitgevoerd.

_beginthread en _beginthreadex zijn vergelijkbaar met de functie CreateThread in de Win32-API, maar heeft deze verschillen:

  • Ze initialiseren bepaalde C-runtimebibliotheekvariabelen. Dat is alleen belangrijk als u de C-runtimebibliotheek in uw threads gebruikt.

  • CreateThread helpt controle te bieden over beveiligingskenmerken. U kunt deze functie gebruiken om een thread in een onderbroken status te starten.

_beginthread en _beginthreadex geven een handle naar de nieuwe thread als het succesvol is of een foutcode bij een fout.

De functies _endthread en _endthreadex

De functie _endthread beëindigt een thread die is gemaakt door _beginthread (en beëindigt op dezelfde manier _endthreadex een thread die is gemaakt door _beginthreadex). Threads worden automatisch beëindigd wanneer ze zijn voltooid. _endthread en _endthreadex zijn nuttig voor voorwaardelijke beëindiging vanuit een thread. Een thread die is toegewezen aan communicatieverwerking, kan bijvoorbeeld worden afgesloten als de communicatiepoort niet kan worden gecontroleerd.

Een multithreaded Win32-programma schrijven

Wanneer u een programma met meerdere threads schrijft, moet u het gedrag en het gebruik van de resources van het programma coördineren. Zorg er ook voor dat elke thread een eigen stack ontvangt.

Gemeenschappelijke hulpmiddelen delen tussen threads

Opmerking

Zie Multithreading: Programmeertips en Multithreading: Wanneer u de synchronisatieklassen gebruikt voor een vergelijkbare discussie vanuit het oogpunt van MFC.

Elke thread heeft een eigen stack en een eigen kopie van de CPU-registers. Andere resources, zoals bestanden, statische gegevens en heapgeheugen, worden gedeeld door alle threads in het proces. Threads die deze algemene resources gebruiken, moeten worden gesynchroniseerd. Win32 biedt verschillende manieren om bronnen te synchroniseren, waaronder semaforen, kritieke secties, gebeurtenissen en mutexes.

Wanneer meerdere threads toegang hebben tot statische gegevens, moet uw programma zorgen voor mogelijke resourceconflicten. Overweeg een programma waarbij een thread een statische gegevensstructuur met x,y-coördinaten bijwerken voor items die door een andere thread moeten worden weergegeven. Als de updatethread de x-coördinaat wijzigt en wordt onderbroken voordat de y-coördinaat kan worden gewijzigd, kan de weergavethread worden ingepland voordat de y-coördinaat wordt bijgewerkt. Het item wordt weergegeven op de verkeerde locatie. U kunt dit probleem voorkomen door semaforen te gebruiken om de toegang tot de structuur te beheren.

Een mutex (kort voor mutual exclusion) is een manier om te communiceren tussen threads of processen die asynchroon ten opzichte van elkaar worden uitgevoerd. Deze communicatie kan worden gebruikt om de activiteiten van meerdere threads of processen te coördineren, meestal door de toegang tot een gedeelde resource te beheren door de resource te vergrendelen en te ontgrendelen. Om dit x,y coördinaten updateprobleem op te lossen, stelt de updatethread een mutex in die aangeeft dat de gegevensstructuur in gebruik is, voordat de update wordt uitgevoerd. Het zou de mutex wissen nadat beide coördinaten waren verwerkt. De weergavethread moet wachten totdat de mutex leeg is voordat de weergave wordt bijgewerkt. Dit proces van wachten op een mutex wordt vaak blokkeren op een mutex genoemd, omdat het proces wordt geblokkeerd en niet kan worden voortgezet totdat de mutex wordt gewist.

Het programma Bounce.c dat wordt weergegeven in Voorbeeld van een multithread C-programma gebruikt een mutex genaamd ScreenMutex om schermupdates te coördineren. Telkens wanneer een van de weergavethreads klaar is om naar het scherm te schrijven, roept WaitForSingleObject deze aan met de greep naar ScreenMutex en constant INFINITE om aan te geven dat de WaitForSingleObject aanroep moet worden geblokkeerd op de mutex en geen time-out. Als ScreenMutex dit duidelijk is, stelt de wachtfunctie de mutex in, zodat andere threads de weergave niet kunnen verstoren en doorgaan met het uitvoeren van de thread. Anders blokkeert de thread totdat de mutex wordt gewist. Wanneer de thread de weergave-update voltooit, wordt de mutex vrijgegeven door aan te roepen ReleaseMutex.

Schermweergaven en statische gegevens zijn slechts twee van de resources waarvoor zorgvuldig beheer is vereist. Uw programma kan bijvoorbeeld meerdere threads hebben die toegang hebben tot hetzelfde bestand. Omdat een andere thread de bestandswijzer mogelijk heeft verplaatst, moet elke thread de bestandswijzer opnieuw instellen voordat deze wordt gelezen of geschreven. Bovendien moet elke thread ervoor zorgen dat deze niet wordt onderbroken tussen het moment waarop de wijzer wordt geplaatst en het moment waarop het toegang krijgt tot het bestand. Deze threads moeten een semaphore gebruiken om de toegang tot het bestand te coördineren door elke bestandstoegang met WaitForSingleObject en ReleaseMutex aanroepen te koppelen. In het volgende codevoorbeeld ziet u deze techniek:

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

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

Threadstacks

Alle standaardstackruimte van een toepassing wordt toegewezen aan de eerste thread van de uitvoering, ook wel thread 1 genoemd. Als gevolg hiervan moet u opgeven hoeveel geheugen moet worden toegewezen voor een afzonderlijke stack voor elke extra thread die uw programma nodig heeft. Het besturingssysteem wijst indien nodig extra stackruimte toe voor de thread, maar u moet een standaardwaarde opgeven.

Het eerste argument in de _beginthread aanroep is een aanwijzer naar de BounceProc functie, waarmee de threads worden uitgevoerd. Met het tweede argument wordt de standaardstackgrootte voor de thread opgegeven. Het laatste argument is een id-nummer dat wordt doorgegeven aan BounceProc. BounceProc gebruikt het id-nummer om de generator voor willekeurige getallen te seeden en om het kleurkenmerk en het weergaveteken van de thread te selecteren.

Threads die aanroepen naar de C-runtimebibliotheek of de Win32-API maken, moeten voldoende stackruimte toestaan voor de bibliotheek- en API-functies die ze aanroepen. De C-functie printf vereist meer dan 500 bytes aan stackruimte en u moet 2K bytes aan stackruimte beschikbaar hebben bij het aanroepen van Win32 API-routines.

Omdat elke thread een eigen stack heeft, kunt u mogelijke conflicten over gegevensitems voorkomen door zo weinig mogelijk statische gegevens te gebruiken. Ontwerp uw programma voor het gebruik van automatische stackvariabelen voor alle gegevens die privé kunnen zijn voor een thread. De enige globale variabelen in het programma Bounce.c zijn mutexes of variabelen die nooit veranderen nadat ze zijn geïnitialiseerd.

Win32 biedt ook TLS (Thread-local Storage) voor het opslaan van gegevens per thread. Zie De lokale opslag van threads (TLS) voor meer informatie.

Problemen met multithread-programma's vermijden

Er zijn verschillende problemen die kunnen optreden bij het maken, koppelen of uitvoeren van een multithread C-programma. Enkele veelvoorkomende problemen worden beschreven in de volgende tabel. (Zie Multithreading: Programmeertips voor een vergelijkbare discussie vanuit het oogpunt van MFC.)

Probleem Waarschijnlijke oorzaak
Er wordt een berichtvenster weergegeven waarin wordt weergegeven dat uw programma een beveiligingsschending heeft veroorzaakt. Veel Win32-programmeerfouten veroorzaken beveiligingsschendingen. Een veelvoorkomende oorzaak van beveiligingsschendingen is de indirecte toewijzing van gegevens aan null-aanwijzers. Omdat dit ertoe leidt dat uw programma toegang probeert te krijgen tot geheugen dat er niet bij hoort, wordt er een beveiligingsschending uitgegeven.

Een eenvoudige manier om de oorzaak van een beveiligingsschending te detecteren, is door uw programma te compileren met foutopsporingsgegevens en deze vervolgens uit te voeren via het foutopsporingsprogramma in de Visual Studio-omgeving. Wanneer de beveiligingsfout optreedt, geeft Windows de controle over aan de debugger, en wordt de cursor naar de regel geplaatst die het probleem heeft veroorzaakt.
Uw programma genereert talloze compilatie- en koppelingsfouten. U kunt veel mogelijke problemen elimineren door het waarschuwingsniveau van de compiler in te stellen op een van de hoogste waarden en de waarschuwingsberichten te volgen. Met de opties voor waarschuwingsniveau niveau 3 of niveau 4 kunt u onbedoelde gegevensconversies, ontbrekende functieprototypes en het gebruik van niet-ANSI-functies detecteren.

Zie ook

Ondersteuning voor multithreading voor oudere code (Visual C++)
Voorbeeld van multithread-programma in C
Lokale threadopslag (TLS)
gelijktijdigheid en asynchrone bewerkingen met C++/WinRT-
Multithreading met C++ en MFC