Проблемы синхронизации и многопроцессора
Приложения могут столкнуться с проблемами при запуске в многопроцессорных системах из-за предположений, которые они делают допустимыми только в однопроцессорных системах.
Приоритеты потоков
Рассмотрим программу с двумя потоками, одной из них с более высоким приоритетом, чем другая. В однопроцессорной системе поток с более высоким приоритетом не откажется от управления более низким приоритетом, так как планировщик предоставляет предпочтение потокам с более высоким приоритетом. В многопроцессорной системе обе потоки могут выполняться одновременно, каждый из них имеет собственный процессор.
Приложения должны синхронизировать доступ к структурам данных, чтобы избежать условий гонки. Код, предполагающий, что потоки с более высоким приоритетом выполняются без помех из потоков с низким приоритетом, завершаются сбоем в системах с несколькими обработчиками.
Упорядочение памяти
При записи процессора в расположение памяти значение кэшируется для повышения производительности. Аналогичным образом процессор пытается удовлетворить запросы на чтение из кэша, чтобы повысить производительность. Кроме того, процессоры начинают извлекать значения из памяти, прежде чем они запрашиваются приложением. Это может произойти в рамках спекулятивного выполнения или из-за проблем с строкой кэша.
Кэши ЦП можно секционировать в банки, к которым можно получить доступ параллельно. Это означает, что операции с памятью можно завершить вне порядка. Чтобы обеспечить выполнение операций памяти в порядке, большинство процессоров предоставляют инструкции по барьеру памяти. полный барьер памяти гарантирует, что операции чтения и записи памяти, которые отображаются перед инструкцией барьера памяти, фиксируются в памяти перед любыми операциями чтения и записи памяти, которые появляются после инструкции барьера памяти. Барьер чтения памяти упорядочивает только операции чтения памяти и барьер записи памяти заказывает только операции записи памяти. Эти инструкции также гарантируют, что компилятор отключает любые оптимизации, которые могут переупорядочение операций памяти по барьерам.
Процессоры могут поддерживать инструкции по барьерам памяти с семантикой получения, выпуска и ограждения. Эти семантики описывают порядок, в котором результаты операции становятся доступными. При получении семантики результаты операции доступны до результатов любой операции, которая отображается после него в коде. Семантикой выпуска результаты операции доступны после результатов любой операции, которая отображается перед ним в коде. Семантика забора объединяет семантику получения и выпуска. Результаты операции с семантикой ограждения доступны перед любой операцией, которая отображается после нее в коде и после любой операции, которая отображается перед ней.
На процессорах 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;
}
Связанные разделы