Share via


Problemi di sincronizzazione e multiprocessore

Le applicazioni possono riscontrare problemi durante l'esecuzione su sistemi multiprocessore a causa di presupposti che sono validi solo nei sistemi a processore singolo.

Priorità dei thread

Si consideri un programma con due thread, uno con una priorità più alta rispetto all'altra. In un sistema a processore singolo, il thread con priorità più alta non rinuncerà al thread con priorità più bassa perché l'utilità di pianificazione assegna la preferenza ai thread con priorità più alta. In un sistema multiprocessore, entrambi i thread possono essere eseguiti contemporaneamente, ognuno nel proprio processore.

Le applicazioni devono sincronizzare l'accesso alle strutture di dati per evitare race condition. Il codice che presuppone che i thread con priorità più alta vengano eseguiti senza interferenze dai thread con priorità inferiore avranno esito negativo nei sistemi multiprocessore.

Ordinamento memoria

Quando un processore scrive in una posizione di memoria, il valore viene memorizzato nella cache per migliorare le prestazioni. Analogamente, il processore tenta di soddisfare le richieste di lettura dalla cache per migliorare le prestazioni. Inoltre, i processori iniziano a recuperare i valori dalla memoria prima che vengano richiesti dall'applicazione. Ciò può verificarsi come parte dell'esecuzione speculativa o a causa di problemi di riga della cache.

Le cache della CPU possono essere partizionate in banche a cui è possibile accedere in parallelo. Ciò significa che le operazioni di memoria possono essere completate in ordine non corretto. Per assicurarsi che le operazioni di memoria vengano completate in ordine, la maggior parte dei processori fornisce istruzioni sulle barriere di memoria. Una barriera di memoria completa garantisce che le operazioni di lettura e scrittura della memoria visualizzate prima dell'istruzione della barriera di memoria vengano sottoposte a commit in memoria prima di qualsiasi operazione di lettura e scrittura della memoria visualizzate dopo l'istruzione della barriera di memoria. Una barriera di memoria di lettura ordina solo le operazioni di lettura della memoria e una barriera di memoria di scrittura ordina solo le operazioni di scrittura della memoria. Queste istruzioni assicurano inoltre che il compilatore disabiliti tutte le ottimizzazioni che potrebbero riordinare le operazioni di memoria attraverso le barriere.

I processori possono supportare istruzioni per le barriere di memoria con semantica di acquisizione, rilascio e isolamento. Queste semantiche descrivono l'ordine in cui i risultati di un'operazione diventano disponibili. Con la semantica di acquisizione, i risultati dell'operazione sono disponibili prima dei risultati di qualsiasi operazione visualizzata dopo di esso nel codice. Con la semantica di rilascio, i risultati dell'operazione sono disponibili dopo i risultati di qualsiasi operazione visualizzata prima di esso nel codice. La semantica di isolamento combina semantica di acquisizione e rilascio. I risultati di un'operazione con semantica di isolamento sono disponibili prima di quelle di qualsiasi operazione visualizzata dopo di essa nel codice e dopo quelle di qualsiasi operazione visualizzata prima.

Nei processori x86 e x64 che supportano S edizione Standard 2, le istruzioni sono mfence (recinto di memoria), lfence (recinzione di carico) e sfence (recinto di archiviazione). Nei processori ARM, le istruzioni sono dmb e dsb. Per altre informazioni, vedere la documentazione relativa al processore.

Le funzioni di sincronizzazione seguenti usano le barriere appropriate per garantire l'ordinamento della memoria:

  • Funzioni che immettono o lasciano sezioni critiche
  • Funzioni che acquisiscono o rilasciano blocchi SRW
  • Inizio e completamento dell'inizializzazione una tantum
  • Funzione EnterSynchronizationBarrier
  • Funzioni che segnalano oggetti di sincronizzazione
  • Funzioni di attesa
  • Funzioni interlocked (ad eccezione delle funzioni con suffisso NoFence o intrinseci con suffisso _nf )

Correzione di una race condition

Il codice seguente presenta una race condition in un sistema multiprocessore perché il processore che esegue CacheComputedValue la prima volta può scrivere fValueHasBeenComputed nella memoria principale prima di scrivere iValue nella memoria principale. Di conseguenza, un secondo processore in esecuzione FetchComputedValue contemporaneamente legge fValueHasBeenComputed come TRUE, ma il nuovo valore di iValue è ancora nella cache del primo processore e non è stato scritto in memoria.

int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (!fValueHasBeenComputed) 
  {
    iValue = ComputeValue();
    fValueHasBeenComputed = TRUE;
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (fValueHasBeenComputed) 
  {
    *piResult = iValue;
    return TRUE;
  } 

  else return FALSE;
}

Questa race condition precedente può essere ripristinata usando la parola chiave volatile o la funzione InterlockedExchange per assicurarsi che il valore di iValue venga aggiornato per tutti i processori prima che il valore di fValueHasBeenComputed sia impostato su TRUE.

A partire da Visual Studio 2005, se compilato in modalità /volatile:ms , il compilatore usa la semantica di acquisizione per le operazioni di lettura su variabili volatili e semantica di rilascio per operazioni di scrittura su variabili volatili (se supportate dalla CPU). Pertanto, è possibile correggere l'esempio come segue:

volatile int iValue;
volatile BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (!fValueHasBeenComputed) 
  {
    iValue = ComputeValue();
    fValueHasBeenComputed = TRUE;
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (fValueHasBeenComputed) 
  {
    *piResult = iValue;
    return TRUE;
  } 

  else return FALSE;
}

Con Visual Studio 2003, i riferimenti volatili a volatili vengono ordinati; il compilatore non riordina l'accesso alle variabili volatili. Tuttavia, queste operazioni potrebbero essere riordinate dal processore. Pertanto, è possibile correggere l'esempio come segue:

int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();

void CacheComputedValue()
{
  if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed, 
          FALSE, FALSE)==FALSE) 
  {
    InterlockedExchange ((LONG*)&iValue, (LONG)ComputeValue());
    InterlockedExchange ((LONG*)&fValueHasBeenComputed, TRUE);
  }
}
 
BOOL FetchComputedValue(int *piResult)
{
  if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed, 
          TRUE, TRUE)==TRUE) 
  {
    InterlockedExchange((LONG*)piResult, (LONG)iValue);
    return TRUE;
  } 

  else return FALSE;
}

Oggetti sezione critica

Accesso a variabili interlocked

Funzioni di attesa