Udostępnij za pośrednictwem


Problemy z synchronizacją i wieloma procesorami

Aplikacje mogą napotkać problemy podczas uruchamiania w systemach wieloprocesorowych ze względu na założenia, które są prawidłowe tylko w systemach z jednym procesorem.

Priorytety wątków

Rozważmy program z dwoma wątkami, jeden z wyższym priorytetem niż drugi. W systemie z jednym procesorem wątek o wyższym priorytcie nie będzie usuwać kontroli z wątkiem o niższym priorytcie, ponieważ harmonogram daje preferencję wątkom o wyższym priorytcie. W systemie wieloprocesorowym oba wątki mogą być uruchamiane jednocześnie, z których każdy ma własny procesor.

Aplikacje powinny synchronizować dostęp do struktur danych, aby uniknąć warunków wyścigu. Kod, który zakłada, że wątki o wyższym priorytcie są uruchamiane bez ingerencji z wątków o niższym priorytcie, nie będą działać w systemach wieloprocesorowych.

Porządkowanie pamięci

Gdy procesor zapisuje w lokalizacji pamięci, wartość jest buforowana w celu zwiększenia wydajności. Podobnie procesor próbuje spełnić żądania odczytu z pamięci podręcznej w celu zwiększenia wydajności. Ponadto procesory zaczynają pobierać wartości z pamięci, zanim będą żądane przez aplikację. Może się to zdarzyć w ramach wykonywania spekulatywnego lub z powodu problemów z wierszem pamięci podręcznej.

Pamięci podręczne procesora CPU można podzielić na banki, do których można uzyskać dostęp równolegle. Oznacza to, że operacje pamięci można wykonać poza kolejnością. Aby upewnić się, że operacje pamięci są wykonywane w kolejności, większość procesorów udostępnia instrukcje dotyczące bariery pamięci. pełną barierę pamięci gwarantuje, że operacje odczytu i zapisu pamięci, które pojawiają się przed wykonaniem instrukcji bariery pamięci zostaną zatwierdzone do pamięci przed wszelkimi operacjami odczytu i zapisu pamięci, które pojawiają się po instrukcji bariery pamięci. Bariera pamięci odczytu porządkuje tylko operacje odczytu pamięci i barierę pamięci zapisu porządkuje tylko operacje zapisu pamięci. Te instrukcje zapewniają również, że kompilator wyłącza wszelkie optymalizacje, które mogą zmienić kolejność operacji pamięci w barierach.

Procesory mogą obsługiwać instrukcje dotyczące barier pamięci z semantyki uzyskiwania, wydawania i ogrodzenia. Semantyka ta opisuje kolejność, w której wyniki operacji stają się dostępne. W przypadku semantyki uzyskiwania wyniki operacji są dostępne przed wynikami dowolnej operacji, która pojawia się po niej w kodzie. W przypadku semantyki wydania wyniki operacji są dostępne po wynikach każdej operacji, która pojawia się przed nim w kodzie. Semantyka ogrodzenia łączy semantyka pozyskiwania i wydawania. Wyniki operacji z semantykami ogrodzenia są dostępne przed tymi, które pojawiają się po niej w kodzie i po wykonaniu jakiejkolwiek operacji, która zostanie wyświetlona przed nią.

W przypadku procesorów x86 i x64 obsługujących SSE2 instrukcje są mfence (ogrodzenie pamięci), lfence (ogrodzenie obciążenia) i sfence (ogrodzenie sklepu). W przypadku procesorów ARM instrucje są dmb i dsb. Aby uzyskać więcej informacji, zobacz dokumentację procesora.

Następujące funkcje synchronizacji używają odpowiednich barier w celu zapewnienia kolejności pamięci:

  • Funkcje, które wprowadzają lub opuszczają sekcje krytyczne
  • Funkcje, które uzyskują lub zwalniają blokady SRW
  • Rozpoczynanie i kończenie jednorazowej inicjowania
  • funkcja EnterSynchronizationBarrier
  • Funkcje sygnalizujące obiekty synchronizacji
  • Funkcje oczekiwania
  • Funkcje połączone (z wyjątkiem funkcji z sufiksem NoFence lub funkcjami wewnętrznymi z sufiksem _nf)

Naprawianie stanu wyścigu

Poniższy kod ma warunek wyścigu w systemach wieloprocesorowych, ponieważ procesor wykonujący CacheComputedValue po raz pierwszy może zapisywać fValueHasBeenComputed pamięci głównej przed zapisaniem iValue do pamięci głównej. W związku z tym drugi procesor wykonujący FetchComputedValue jednocześnie odczytuje fValueHasBeenComputed co true, ale nowa wartość iValue jest nadal w pamięci podręcznej pierwszego procesora i nie została zapisana w pamięci.

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

Ten warunek wyścigu powyżej można naprawić przy użyciu volatile słowa kluczowego lub InterlockedExchange funkcji, aby upewnić się, że wartość iValue jest aktualizowana dla wszystkich procesorów przed ustawieniem wartości fValueHasBeenComputed na TRUE.

Począwszy od programu Visual Studio 2005, jeśli kompilowany w trybie /volatile:ms, kompilator używa semantyki na potrzeby operacji odczytu na zmiennych lotnych i semantyki wydania dla operacji zapisu na zmienne lotne (jeśli są obsługiwane przez procesor CPU). W związku z tym można poprawić przykład w następujący sposób:

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

W programie Visual Studio 2003 lotnenietrwałe odwołania są uporządkowane; kompilator nie będzie ponownie porządkować lotnych dostępu do zmiennych. Jednak te operacje mogą być ponownie uporządkowane przez procesor. W związku z tym można poprawić przykład w następujący sposób:

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

obiekty sekcji krytycznej

dostępu do zmiennych połączonych

funkcje oczekiwania