Partager via


Problèmes de synchronisation et de multiprocesseur

Les applications peuvent rencontrer des problèmes lorsqu’elles s’exécutent sur des systèmes multiprocesseurs en raison d’hypothèses qu’elles font, qui ne sont valides que sur les systèmes à processeur unique.

Priorités des threads

Envisagez d’utiliser un programme avec deux threads, l’un avec une priorité plus élevée que l’autre. Sur un système à processeur unique, le thread de priorité plus élevée n’abandonne pas le contrôle au thread de priorité plus basse, car le planificateur donne la préférence aux threads de priorité élevée. Sur un système multiprocesseur, les deux threads peuvent s’exécuter simultanément, chacun sur son propre processeur.

Les applications doivent synchroniser l’accès aux structures de données pour éviter les conditions d’engorgement. Le code qui suppose que les threads de priorité plus élevée s’exécutent sans interférence de priorité échoue sur les systèmes multiprocesseurs.

Ordonnancement en mémoire

Lorsqu’un processeur écrit dans un emplacement de mémoire, la valeur est mise en cache pour améliorer les performances. De même, le processeur tente de satisfaire les demandes de lecture du cache pour améliorer les performances. En outre, les processeurs commencent à récupérer des valeurs de la mémoire avant qu’elles ne soient demandées par l’application. Cela peut se produire dans le cadre de l’exécution spéculative ou en raison de problèmes de ligne de cache.

Les caches d’UC peuvent être partitionnés en banques accessibles en parallèle. Cela signifie que les opérations de mémoire peuvent être terminées en dehors de l’ordre. Pour vous assurer que les opérations de mémoire sont effectuées dans l’ordre, la plupart des processeurs fournissent des instructions de barrière mémoire. Une barrière mémoire complète garantit que les opérations de lecture et d’écriture de la mémoire qui apparaissent avant l’instruction de barrière mémoire sont validées en mémoire avant toute opération qui apparaîtrait après l’instruction de barrière mémoire. Une barrière mémoire en lecture commande uniquement les opérations de lecture de mémoire et une barrière mémoire en écriture commande uniquement les opérations d’écriture de mémoire. Ces instructions garantissent également que le compilateur désactive toutes les optimisations susceptibles de réorganiser les opérations de mémoire dans les barrières.

Les processeurs peuvent prendre en charge des instructions pour les barrières mémoire avec la sémantique d’acquisition, de mise en production et de clôture. Ces sémantiques décrivent l’ordre dans lequel les résultats d’une opération deviennent disponibles. Avec la sémantique d’acquisition, les résultats de l’opération sont disponibles avant les résultats d’une opération qui apparaît après dans le code. Avec la sémantique de mise en production, les résultats de l’opération sont disponibles après les résultats d’une opération qui apparaît avant dans le code. La sémantique de clôture combine la sémantique d’acquisition et de mise en production. Les résultats d’une opération avec sémantique de clôture sont disponibles avant ceux d’une opération suivante dans le code et après ceux d’une opération préalable dans le code.

Sur les processeurs x86 et x64 qui prennent en charge SSE2, les instructions sont mfence (clôture mémoire), lfence (clôture de charge) et sfence (clôture de magasin). Sur les processeurs ARM, les instructions sont dmb et dsb. Pour plus d’informations, consultez la documentation du processeur.

Les fonctions de synchronisation suivantes utilisent les barrières appropriées pour garantir l’ordonnancement de la mémoire :

  • Fonctions qui entrent ou quittent des sections critiques
  • Fonctions qui acquièrent ou libèrent des verrous SRW
  • Début et achèvement de l’initialisation à usage unique
  • Fonction EnterSynchronizationBarrier
  • Fonctions qui signalent les objets de synchronisation
  • Fonctions d’attente
  • Fonctions Interlocked (à l’exception des fonctions avec le suffixe NoFence ou intrinsèques au suffixe _nf)

Résolution d’une condition d’engorgement

Le code suivant présente une condition d’engorgement sur un système multiprocesseur, car le processeur qui exécute CacheComputedValue la première fois peut écrire fValueHasBeenComputed avant d’écrire iValue dans la mémoire principale. Par conséquent, un deuxième processeur exécutant FetchComputedValue en même temps lit fValueHasBeenComputed comme TRUE, mais la nouvelle valeur de iValue est toujours dans le cache du premier processeur et n’a pas été écrite en mémoire.

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;
}

Cette condition d’engorgement ci-dessus peut être réparée à l’aide du mot clé volatile ou de la fonction InterlockedExchange pour s’assurer que la valeur de iValue soit mise à jour pour tous les processeurs avant que la valeur de fValueHasBeenComputed soit indiquée comme TRUE.

À compter de Visual Studio 2005, s’il est compilé en mode /volatile:ms, le compilateur utilise une sémantique d’acquisition pour les opérations de lecture sur les variables volatiles et la sémantique de mise en production pour les opérations d’écriture sur les variables volatiles (lorsqu’il est pris en charge par l’UC). Par conséquent, vous pouvez corriger l’exemple comme suit :

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;
}

Avec Visual Studio 2003, les références volatiles aux références volatiles sont mises en ordre. Le compilateur ne réordonnera pas l’accès aux variables volatiles. Toutefois, ces opérations peuvent être réordonnées par le processeur. Par conséquent, vous pouvez corriger l’exemple comme suit :

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;
}

Objets de section critique

Accès aux variables Interlocked

Fonctions d’attente