同步處理和多處理器問題

應用程式在多處理器系統上執行時可能會遇到問題,因為假設它們只在單處理器系統上有效。

執行緒優先順序

請考慮具有兩個線程的程式,一個優先順序高於另一個線程。 在單一處理器系統上,較高優先順序的線程不會放棄對較低優先順序線程的控制,因為排程器會優先使用較高優先順序的線程。 在多處理器系統上,這兩個線程都可以同時執行,每個線程都會在其本身的處理器上執行。

應用程式應該同步處理數據結構的存取,以避免競爭狀況。 假設較高優先順序的線程會在多處理器系統上執行,而不會干擾較低優先順序線程的程序代碼。

記憶體排序

當處理器寫入記憶體位置時,會快取值以改善效能。 同樣地,處理器會嘗試滿足快取的讀取要求,以改善效能。 此外,處理器會先從記憶體擷取值,再由應用程式要求這些值。 這可能會在推測性執行或快取行問題時發生。

CPU 快取可以分割成可以平行存取的銀行。 這表示記憶體作業可以依序完成。 為了確保記憶體作業依序完成,大部分處理器都會提供記憶體屏障指示。 完整記憶體屏障可確保記憶體屏障指令之前出現的記憶體讀取和寫入作業會認可記憶體,再認可記憶體屏障指令之後出現的任何記憶體讀取和寫入作業。 讀取記憶體屏障只會排序記憶體讀取作業,而寫入記憶體屏障只會排序記憶體寫入作業。 這些指示也可確保編譯程式會停用任何可跨屏障重新排序記憶體作業的優化。

處理器可以支援具有取得、釋放和圍欄語意之內存屏障的指示。 這些語意描述作業結果可供使用的順序。 透過取得語意,作業的結果可在程式碼中出現的任何作業結果之前取得。 使用發行語意時,作業的結果會在程式代碼中出現的任何作業結果之後取得。 柵欄語意結合了取得和釋放語意。 具有圍欄語意的作業結果可在程式代碼中出現的任何作業之後,以及出現在程式代碼之前的任何作業之後取得。

在支援 SSE2 的 x86 和 x64 處理器上,指示為 mfence (記憶體柵欄)、lfence(負載柵欄)和欄(商店柵欄)。 在 ARM 處理器上,入侵是 dmbdsb。 如需詳細資訊,請參閱處理器的檔。

下列同步處理函式會使用適當的屏障來確保記憶體順序:

  • 輸入或離開重要區段的函式
  • 取得或釋放 SRW 鎖定的函式
  • 一次性初始化開始和完成
  • EnterSynchronizationBarrier 函
  • 發出同步處理對象的訊號函式
  • 等候函式
  • 聯結函式(除了具有 NoFence 後綴的函式,或具有 後置詞_nf 的內建函式除外)

修正競爭條件

下列程式代碼在多處理器系統上有競爭條件,因為第一次執行的CacheComputedValue處理器可能會在寫入iValue主要記憶體之前寫入fValueHasBeenComputed主要記憶體。 因此,同時執行 FetchComputedValue 的第二個處理器會讀取 fValueHasBeenComputedTRUE,但的新值 iValue 仍在第一個處理器的快取中,而且尚未寫入記憶體。

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

您可以使用 volatile 關鍵詞或 InterlockedExchange 函式來修復上述競爭條件,以確保 在所有處理器的值設定為 TRUE 之前更新的值iValuefValueHasBeenComputed

從 Visual Studio 2005 開始,如果在 /volatile:ms 模式中編譯,編譯程式會使用取得語意來讀取動態變數的語意,並釋放動態變數上寫入作業的語意(由 CPU 支援時)。 因此,您可以更正範例,如下所示:

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

使用 Visual Studio 2003 時,會排序動態至動態參考;編譯程式不會重新排序動態變數存取。 不過,處理器可以重新排序這些作業。 因此,您可以更正範例,如下所示:

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

重要區段物件

Interlocked Variable Access

等候函式