Multithreading con C e Win32

Il compilatore Microsoft C/C++ (MSVC) fornisce il supporto per la creazione di applicazioni multithread. Prendere in considerazione l'uso di più thread se l'applicazione deve eseguire operazioni costose che potrebbero causare la mancata risposta dell'interfaccia utente.

Con MSVC sono disponibili diversi modi per programmare con più thread: è possibile usare C++/WinRT e la libreria Windows Runtime, la libreria MFC (Microsoft Foundation Class), C++/CLI e il runtime .NET o la libreria di runtime C e l'API Win32. Questo articolo riguarda il multithreading in C. Per un esempio di codice, vedere Esempio di programma multithread in C.

Programmi multithread

Un thread è fondamentalmente un percorso di esecuzione tramite un programma. È anche l'unità di esecuzione più piccola pianificata da Win32. Un thread è costituito da uno stack, dallo stato dei registri della CPU e da una voce nell'elenco di esecuzione dell'utilità di pianificazione di sistema. Ogni thread condivide tutte le risorse del processo.

Un processo è costituito da uno o più thread e il codice, i dati e altre risorse di un programma in memoria. Le risorse di programma tipiche sono file aperti, semafori e memoria allocata dinamicamente. Un programma viene eseguito quando l'utilità di pianificazione di sistema fornisce un controllo di esecuzione dei thread. L'utilità di pianificazione determina quali thread devono essere eseguiti e quando devono essere eseguiti. I thread con priorità inferiore potrebbero dover attendere mentre i thread con priorità più alta completano le attività. Nei computer multiprocessore, l'utilità di pianificazione può spostare singoli thread in processori diversi per bilanciare il carico della CPU.

Ogni thread in un processo opera in modo indipendente. A meno che non siano visibili tra loro, i thread vengono eseguiti singolarmente e non sono a conoscenza degli altri thread in un processo. I thread che condividono risorse comuni, tuttavia, devono coordinare il lavoro usando semafori o un altro metodo di comunicazione interprocesso. Per altre informazioni sulla sincronizzazione dei thread, vedere Scrittura di un programma Win32 multithreading.

Supporto della libreria per il multithreading

Tutte le versioni di CRT supportano ora il multithreading, ad eccezione delle versioni non di blocco di alcune funzioni. Per altre informazioni, vedere Prestazioni delle librerie multithreading. Per informazioni sulle versioni di CRT disponibili per il collegamento al codice, vedere Funzionalità della libreria CRT.

File di inclusione per il multithreading

I file di inclusione CRT standard dichiarano le funzioni della libreria di runtime C durante l'implementazione nelle librerie. Se le opzioni del compilatore specificano le convenzioni di chiamata __fastcall o __vectorcall , il compilatore presuppone che tutte le funzioni vengano chiamate usando la convenzione di chiamata del registro. Le funzioni della libreria di runtime usano la convenzione di chiamata C e le dichiarazioni nei file di inclusione standard indicano al compilatore di generare riferimenti esterni corretti a queste funzioni.

Funzioni CRT per il controllo thread

Tutti i programmi Win32 hanno almeno un thread. Qualsiasi thread può creare thread aggiuntivi. Un thread può completare rapidamente il suo lavoro e quindi terminare, oppure può rimanere attivo per la vita del programma.

Le librerie CRT forniscono le funzioni seguenti per la creazione e la terminazione dei thread: _beginthread, _beginthreadex, _endthread e _endthreadex.

Le _beginthread funzioni e _beginthreadex creano un nuovo thread e restituiscono un identificatore di thread se l'operazione ha esito positivo. Il thread termina automaticamente se completa l'esecuzione. In alternativa, può terminare se stesso con una chiamata a _endthread o _endthreadex.

Nota

Se si chiamano routine di runtime C da un programma compilato con libcmt.lib, è necessario avviare i thread con la _beginthread funzione o _beginthreadex . Non usare le funzioni ExitThread Win32 e CreateThread. L'uso SuspendThread di può causare un deadlock quando più thread sono bloccati in attesa che il thread sospeso completi l'accesso a una struttura di dati in fase di esecuzione C.

Funzioni _beginthread e _beginthreadex

Le _beginthread funzioni e _beginthreadex creano un nuovo thread. Un thread condivide il codice e i segmenti di dati di un processo con altri thread nel processo, ma ha valori di registro univoci, spazio dello stack e indirizzo di istruzione corrente. Il sistema concede tempo cpu a ogni thread, in modo che tutti i thread in un processo possano essere eseguiti simultaneamente.

_beginthread e _beginthreadex sono simili alla funzione CreateThread nell'API Win32, ma presenta queste differenze:

  • Inizializzano determinate variabili della libreria di runtime C. Questo è importante solo se si usa la libreria di runtime C nei thread.

  • CreateThread consente di fornire il controllo sugli attributi di sicurezza. È possibile usare questa funzione per avviare un thread in uno stato sospeso.

_beginthread e _beginthreadex restituisce un handle al nuovo thread se ha esito positivo o un codice di errore se si è verificato un errore.

Funzioni di _endthread e _endthreadex

La funzione _endthread termina un thread creato da _beginthread e, analogamente, _endthreadex termina un thread creato da _beginthreadex. I thread terminano automaticamente al termine. _endthread e _endthreadex sono utili per la terminazione condizionale dall'interno di un thread. Un thread dedicato all'elaborazione delle comunicazioni, ad esempio, può uscire se non è in grado di ottenere il controllo della porta di comunicazione.

Scrittura di un programma multithread Win32

Quando si scrive un programma con più thread, è necessario coordinare il comportamento e l'uso delle risorse del programma. Assicurarsi inoltre che ogni thread riceva il proprio stack.

Condivisione di risorse comuni tra thread

Nota

Per una discussione simile dal punto di vista MFC, vedere Multithreading: Programming Tips and Multithreading: When to Use the Synchronization Classes (Multithreading: Suggerimenti per la programmazione e multithreading: Quando usare le classi di sincronizzazione).

Ogni thread ha un proprio stack e una propria copia dei registri della CPU. Altre risorse, ad esempio file, dati statici e memoria heap, vengono condivise da tutti i thread del processo. I thread che usano queste risorse comuni devono essere sincronizzati. Win32 offre diversi modi per sincronizzare le risorse, tra cui semafori, sezioni critiche, eventi e mutex.

Quando più thread accedono ai dati statici, il programma deve fornire possibili conflitti di risorse. Si consideri un programma in cui un thread aggiorna una struttura di dati statica contenente coordinate x,y per gli elementi da visualizzare da un altro thread. Se il thread di aggiornamento modifica la coordinata x e viene annullata prima che possa modificare la coordinata y , il thread di visualizzazione potrebbe essere pianificato prima dell'aggiornamento della coordinata y . L'elemento verrà visualizzato nella posizione errata. È possibile evitare questo problema usando semafori per controllare l'accesso alla struttura.

Un mutex (abbreviazione di mutual ex clusion) è un modo per comunicare tra thread o processi che vengono eseguiti in modo asincrono tra loro. Questa comunicazione può essere usata per coordinare le attività di più thread o processi, in genere controllando l'accesso a una risorsa condivisa bloccando e sbloccando la risorsa. Per risolvere questo problema di aggiornamento delle coordinate x,y, il thread di aggiornamento imposta un mutex che indica che la struttura dei dati è in uso prima di eseguire l'aggiornamento. Cancellava il mutex dopo l'elaborazione di entrambe le coordinate. Il thread di visualizzazione deve attendere che il mutex sia chiaro prima di aggiornare la visualizzazione. Questo processo di attesa di un mutex viene spesso chiamato blocco su un mutex, perché il processo viene bloccato e non può continuare finché il mutex non viene cancellato.

Il programma Bounce.c illustrato in Sample Multithread C Program usa un mutex denominato ScreenMutex per coordinare gli aggiornamenti dello schermo. Ogni volta che uno dei thread di visualizzazione è pronto per scrivere sullo schermo, chiama WaitForSingleObject con l'handle a ScreenMutex e costante INFINITE per indicare che la WaitForSingleObject chiamata deve bloccarsi sul mutex e non scadere. Se ScreenMutex è chiaro, la funzione wait imposta il mutex in modo che gli altri thread non possano interferire con la visualizzazione e continuino a eseguire il thread. In caso contrario, il thread si blocca finché il mutex non viene cancellato. Quando il thread completa l'aggiornamento dello schermo, rilascia il mutex chiamando ReleaseMutex.

Le visualizzazioni dello schermo e i dati statici sono solo due delle risorse che richiedono un'attenta gestione. Ad esempio, il programma potrebbe avere più thread che accedono allo stesso file. Poiché un altro thread potrebbe aver spostato il puntatore al file, ogni thread deve reimpostare il puntatore del file prima di leggere o scrivere. Inoltre, ogni thread deve assicurarsi che non venga superato tra il momento in cui posiziona il puntatore e il momento in cui accede al file. Questi thread devono usare un semaforo per coordinare l'accesso al file tra parentesi quadre ogni accesso ai file con WaitForSingleObject e ReleaseMutex chiamate. L'esempio di codice seguente illustra questa tecnica:

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

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

Stack di thread

Tutto lo spazio dello stack predefinito di un'applicazione viene allocato al primo thread di esecuzione, noto come thread 1. Di conseguenza, è necessario specificare la quantità di memoria da allocare per uno stack separato per ogni thread aggiuntivo necessario al programma. Il sistema operativo alloca spazio dello stack aggiuntivo per il thread, se necessario, ma è necessario specificare un valore predefinito.

Il primo argomento nella _beginthread chiamata è un puntatore alla BounceProc funzione , che esegue i thread. Il secondo argomento specifica le dimensioni dello stack predefinite per il thread. L'ultimo argomento è un numero ID passato a BounceProc. BounceProc usa il numero ID per inizializzare il generatore di numeri casuali e per selezionare l'attributo colore del thread e il carattere di visualizzazione.

I thread che effettuano chiamate alla libreria di runtime C o all'API Win32 devono consentire spazio dello stack sufficiente per la libreria e le funzioni API chiamate. La funzione C printf richiede più di 500 byte di spazio dello stack ed è necessario disporre di 2 KB di spazio nello stack disponibile quando si chiamano routine API Win32.

Poiché ogni thread ha un proprio stack, è possibile evitare potenziali conflitti sugli elementi di dati usando il minor numero possibile di dati statici. Progettare il programma per usare le variabili dello stack automatico per tutti i dati che possono essere privati in un thread. Le uniche variabili globali nel programma Bounce.c sono mutex o variabili che non cambiano mai dopo l'inizializzazione.

Win32 fornisce anche l'archiviazione tls (Thread-local Storage) per archiviare i dati per thread. Per altre informazioni, vedere Archiviazione locale thread (TLS).For more information, see Thread local storage (TLS).

Suggerimenti per evitare problemi relativi ai programmi multithread

Esistono diversi problemi che possono verificarsi durante la creazione, il collegamento o l'esecuzione di un programma C multithread. Alcuni dei problemi più comuni sono descritti nella tabella seguente. Per una discussione simile dal punto di vista MFC, vedere Multithreading: Suggerimenti per la programmazione.

Problema Possibile causa
Viene visualizzata una finestra di messaggio che mostra che il programma ha causato una violazione della protezione. Molti errori di programmazione Win32 causano violazioni della protezione. Una causa comune delle violazioni della protezione è l'assegnazione indiretta dei dati a puntatori Null. Poiché il programma tenta di accedere alla memoria che non lo appartiene, viene generata una violazione di protezione.

Un modo semplice per rilevare la causa di una violazione della protezione consiste nel compilare il programma con informazioni di debug e quindi eseguirlo tramite il debugger nell'ambiente di Visual Studio. Quando si verifica l'errore di protezione, Windows trasferisce il controllo al debugger e il cursore viene posizionato sulla riga che ha causato il problema.
Il programma genera numerosi errori di compilazione e collegamento. È possibile eliminare molti potenziali problemi impostando il livello di avviso del compilatore su uno dei valori più alti e controllando i messaggi di avviso. Usando le opzioni di livello 3 o livello 4 di avviso, è possibile rilevare conversioni di dati non intenzionali, prototipi di funzioni mancanti e l'uso di funzionalità non ANSI.

Vedi anche

Supporto del multithreading per il codice precedente (Visual C++)
Programma multithread di esempio in C
Archiviazione locale thread (TLS)
Concorrenza e operazioni asincrone con C++/WinRT
Multithreading con C++ e MFC