Freigeben über


Synchronisierungs- und Multiprozessorprobleme

Anwendungen können probleme auftreten, wenn sie auf Multiprozessorsystemen ausgeführt werden, da davon ausgegangen wird, dass sie nur auf Einzelprozessorsystemen gültig sind.

Threadprioritäten

Betrachten Sie ein Programm mit zwei Threads, eines mit höherer Priorität als dem anderen. Bei einem System mit einem einzelnen Prozessor gibt der Thread mit höherer Priorität keine Kontrolle an den Thread mit niedrigerer Priorität an, da der Scheduler Threads mit höherer Priorität bevorzugt. Auf einem Multiprozessorsystem können beide Threads gleichzeitig ausgeführt werden, jeweils auf einem eigenen Prozessor.

Anwendungen sollten den Zugriff auf Datenstrukturen synchronisieren, um Rennbedingungen zu vermeiden. Code, der davon ausgeht, dass Threads mit höherer Priorität ohne Störungen von Threads mit niedrigerer Priorität auf Multiprozessorsystemen fehlschlagen.

Speicher sortierung

Wenn ein Prozessor an einen Speicherspeicherort schreibt, wird der Wert zwischengespeichert, um die Leistung zu verbessern. Ebenso versucht der Prozessor, Leseanforderungen aus dem Cache zu erfüllen, um die Leistung zu verbessern. Außerdem beginnen Prozessoren mit dem Abrufen von Werten aus dem Speicher, bevor sie von der Anwendung angefordert werden. Dies kann im Rahmen der spekulativen Ausführung oder aufgrund von Cachezeilenproblemen auftreten.

CPU-Caches können in Banken partitioniert werden, auf die parallel zugegriffen werden kann. Dies bedeutet, dass Speichervorgänge außerhalb der Reihenfolge abgeschlossen werden können. Um sicherzustellen, dass Speichervorgänge in der Reihenfolge abgeschlossen sind, stellen die meisten Prozessoren Anweisungen für Speicherbarrieren bereit. Eine vollständige Speicherbarriere stellt sicher, dass Speicherlese- und Schreibvorgänge, die vor der Speicherbarrierenanweisung angezeigt werden, vor dem Speicherlese- und Schreibvorgang, der nach der Speicherbarriere-Anweisung angezeigt wird, an den Speicher gebunden werden. Eine Lesespeicherbarriere nur die Speicherlesevorgänge und eine Schreibspeicherbarriere nur die Speicherschreibvorgänge anordnet. Diese Anweisungen stellen außerdem sicher, dass der Compiler optimierungen deaktiviert, die Speichervorgänge über die Barrieren neu anordnen können.

Prozessoren können Anweisungen für Speicherbarrieren mit Der Kauf-, Freigabe- und Zaunsemantik unterstützen. Diese Semantik beschreibt die Reihenfolge, in der ergebnisse eines Vorgangs verfügbar werden. Beim Abrufen der Semantik stehen die Ergebnisse des Vorgangs vor den Ergebnissen eines Vorgangs zur Verfügung, der nach dem Vorgang im Code angezeigt wird. Mit der Releasesemantik sind die Ergebnisse des Vorgangs nach den Ergebnissen eines Vorgangs verfügbar, der vor dem Vorgang im Code angezeigt wird. Zaunsemantik kombinieren Akquirieren und Freigeben der Semantik. Die Ergebnisse eines Vorgangs mit Zaunsemantik sind vor denen eines Vorgangs verfügbar, der nach dem Vorgang im Code und nach den Vorgängen vor dem Vorgang angezeigt wird.

Auf x86- und x64-Prozessoren, die SSE2 unterstützen, sind die Anweisungen Mfence (Speicherzaun), Lfence (Ladezaun) und Sfence (Speicherzaun). Auf ARM-Prozessoren sind die Instrutionen dmb und dsb. Weitere Informationen finden Sie in der Dokumentation für den Prozessor.

Die folgenden Synchronisierungsfunktionen verwenden die entsprechenden Barrieren, um die Speicherordnung sicherzustellen:

  • Funktionen, die kritische Abschnitte eingeben oder verlassen
  • Funktionen, die SRW-Sperren abrufen oder freigeben
  • Einmalige Initialisierung beginnt und abgeschlossen
  • EnterSynchronizationBarrier
  • Funktionen, die Synchronisierungsobjekte signalisieren
  • Wartefunktionen
  • Interlocked Functions (außer Funktionen mit NoFence Suffix oder systeminternen Funktionen mit _nf Suffix)

Reparieren einer Racebedingung

Der folgende Code weist eine Racebedingung auf einem Multiprozessorsystem auf, da der Prozessor, der CacheComputedValue ausgeführt wird, fValueHasBeenComputed zum Hauptspeicher schreiben kann, bevor iValue in den Hauptspeicher geschrieben wird. Folglich liest ein zweiter Prozessor, der FetchComputedValue gleichzeitig ausführt, fValueHasBeenComputed wie TRUE, aber der neue Wert von iValue befindet sich weiterhin im Cache des ersten Prozessors und wurde nicht in den Arbeitsspeicher geschrieben.

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

Diese race condition above can be repaired by using the volatile keyword or the InterlockedExchange function to ensure that the value of iValue is updated for all processor before the value of fValueHasBeenComputed is set to TRUE.

Ab Visual Studio 2005, wenn in /volatile:ms Modus kompiliert wird, verwendet der Compiler Semantik zum Lesen von Vorgängen für veränderliche Variablen und gibt Semantik für Schreibvorgänge für veränderliche Variablen (wenn von der CPU unterstützt) frei. Daher können Sie das Beispiel wie folgt korrigieren:

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

Mit Visual Studio 2003 werden veränderliche , um veränderliche Verweise zu, sortiert. der Compiler wird veränderlichen Variablenzugriffs nicht neu anordnen. Diese Vorgänge können jedoch vom Prozessor neu sortiert werden. Daher können Sie das Beispiel wie folgt korrigieren:

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

Kritische Abschnittsobjekte

Interlocked Variable Access

Wait Functions