Проблемы синхронизации и многопроцессорности

Приложения могут столкнуться с проблемами при запуске в многопроцессорных системах из-за предположений, которые они делают допустимыми только в однопроцессорных системах.

Приоритеты потоков

Рассмотрим программу с двумя потоками, одной из них с более высоким приоритетом, чем другая. В однопроцессорной системе поток с более высоким приоритетом не откажется от управления более низким приоритетом, так как планировщик предоставляет предпочтение потокам с более высоким приоритетом. В многопроцессорной системе обе потоки могут выполняться одновременно, каждый из них имеет собственный процессор.

Приложения должны синхронизировать доступ к структурам данных, чтобы избежать условий гонки. Код, предполагающий, что потоки с более высоким приоритетом выполняются без помех из потоков с низким приоритетом, завершаются сбоем в системах с несколькими обработчиками.

Упорядочение памяти

При записи процессора в расположение памяти значение кэшируется для повышения производительности. Аналогичным образом процессор пытается удовлетворить запросы на чтение из кэша, чтобы повысить производительность. Кроме того, процессоры начинают извлекать значения из памяти, прежде чем они запрашиваются приложением. Это может произойти в рамках спекулятивного выполнения или из-за проблем с строкой кэша.

Кэши ЦП можно секционировать в банки, к которым можно получить доступ параллельно. Это означает, что операции с памятью можно завершить вне порядка. Чтобы обеспечить выполнение операций памяти в порядке, большинство процессоров предоставляют инструкции по барьеру памяти. Полный барьер памяти гарантирует, что операции чтения и записи памяти, которые отображаются перед инструкцией барьера памяти, фиксируются в памяти до любых операций чтения и записи памяти, которые появляются после инструкции барьера памяти. Барьер чтения памяти упорядочивает только операции чтения памяти и барьер памяти записи только операции записи памяти. Эти инструкции также гарантируют, что компилятор отключает любые оптимизации, которые могут переупорядочение операций памяти по барьерам.

Процессоры могут поддерживать инструкции по барьерам памяти с семантикой получения, выпуска и ограждения. Эти семантики описывают порядок, в котором результаты операции становятся доступными. При получении семантики результаты операции доступны до результатов любой операции, которая отображается после него в коде. Семантикой выпуска результаты операции доступны после результатов любой операции, которая отображается перед ним в коде. Семантика забора объединяет семантику получения и выпуска. Результаты операции с семантикой ограждения доступны перед любой операцией, которая отображается после нее в коде и после любой операции, которая отображается перед ней.

На процессорах x86 и x64, поддерживающих SSE2, инструкции — mfence (забор памяти), lfence (забор нагрузки) и sfence (забор магазина). На процессорах ARM струкции являются dmb и dsb. Дополнительные сведения см. в документации для процессора.

Следующие функции синхронизации используют соответствующие барьеры для обеспечения упорядочения памяти:

  • Функции, которые вводят или покидают критически важные разделы
  • Функции, которые получают или освобождают блокировки SRW
  • Начало и завершение однократной инициализации
  • Функция EnterSynchronizationBarrier
  • Функции, которые сигнализируют о объектах синхронизации
  • Функции ожидания
  • Заблокированные функции (за исключением функций с суффиксом NoFence или встроенными функциями с _nf суффиксом)

Исправление состояния гонки

Следующий код имеет состояние гонки в многопроцессорных системах, так как процессор, выполняющий CacheComputedValue первый раз, может записывать fValueHasBeenComputed в основную память перед записью iValue в основную память. Следовательно, второй процессор, выполняющийся FetchComputedValue одновременно, считывает fValueHasBeenComputed значение TRUE, но новое значение 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;
}

Это состояние гонки выше можно исправить с помощью переменной ключевое слово или функции InterlockedExchange, чтобы убедиться, что значение iValue обновляется для всех процессоров, прежде чем значение fValueHasBeenComputed равно TRUE.

Начиная с Visual Studio 2005, если компилируется в режиме /volatile:ms, компилятор использует семантику для операций чтения с переменными и семантики выпуска для операций записи с переменными (при поддержке ЦП). Поэтому можно исправить пример следующим образом:

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

Критически важные объекты раздела

Доступ к переменной с блокировкой

Функции ожидания