Чтение взаимосвязанных переменных - PullRequest
25 голосов
/ 23 апреля 2009

Предположим:

A. C ++ под WIN32.

B. Правильно выровненное переменное целое число увеличивается и уменьшается с использованием InterlockedIncrement() и InterlockedDecrement().

__declspec (align(8)) volatile LONG _ServerState = 0;

Если я хочу просто прочитать _ServerState, нужно ли мне читать переменную с помощью функции InterlockedXXX?

Например, я видел такой код:

LONG x = InterlockedExchange(&_ServerState, _ServerState);

и

LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);

Цель состоит в том, чтобы просто прочитать текущее значение _ServerState.

Разве я не могу просто сказать:

if (_ServerState == some value)
{
// blah blah blah
}

Кажется, есть некоторая путаница в отношении этой темы. Я понимаю, что чтение в формате регистра является атомарным в Windows, поэтому я бы предположил, что функция InterlockedXXX не нужна.

Мэтт Дж.


Хорошо, спасибо за ответы. Кстати, это Visual C ++ 2005 и 2008.

Если это правда, я должен использовать функцию InterlockedXXX для считывания значения _ServerState, даже если просто для ясности, как лучше всего это сделать?

LONG x = InterlockedExchange(&_ServerState, _ServerState);

Это побочный эффект изменения значения, когда все, что я действительно хочу сделать, это прочитать его. Мало того, но есть вероятность, что я мог сбросить флаг на неправильное значение, если есть переключение контекста, так как значение _ServerState помещается в стек при подготовке вызова InterlockedExchange().

LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);

Я взял это из примера, который видел на MSDN.
См http://msdn.microsoft.com/en-us/library/ms686355(VS.85).aspx

Все, что мне нужно, это что-то вроде:

lock mov eax, [_ServerState]

В любом случае, суть, которая, как мне показалось, ясна, заключается в том, чтобы обеспечить потокобезопасный доступ к флагу без дополнительных затрат на критическую секцию. Я видел, как LONG использовали этот способ через семейство функций InterlockedXXX(), поэтому мой вопрос.

Хорошо, мы думаем, что хорошее решение этой проблемы чтения текущего значения:

LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);

Ответы [ 10 ]

13 голосов
/ 23 апреля 2009

Это зависит от того, что вы подразумеваете под «целью - просто прочитать текущее значение _ServerState», и это зависит от того, какой набор инструментов и платформу вы используете (вы указываете Win32 и C ++, но не какой компилятор C ++, и что может иметь значение).

Если вы просто хотите прочитать значение таким образом, чтобы оно не было повреждено (т. Е. Если какой-либо другой процессор меняет значение с 0x12345678 на 0x87654321, то ваше чтение получит одно из этих 2 значений, а не 0x12344321), тогда просто чтение будет будьте в порядке, пока переменная:

  • отмечено volatile,
  • правильно выровнен и
  • читать, используя одну инструкцию с размером слова, который процессор обрабатывает атомарно

Ничего из этого не обещает стандарт C / C ++, но Windows и MSVC действительно дают эти гарантии, и я думаю, что большинство компиляторов, нацеленных на Win32, также делают.

Однако, если вы хотите, чтобы ваше чтение было синхронизировано с поведением другого потока, есть некоторая дополнительная сложность. Скажем, у вас есть простой протокол «почтового ящика»:

struct mailbox_struct {
    uint32_t flag;
    uint32_t data;
};
typedef struct mailbox_struct volatile mailbox;


// the global - initialized before wither thread starts

mailbox mbox = { 0, 0 };

//***************************
// Thread A

while (mbox.flag == 0) { 
    /* spin... */ 
}

uint32_t data = mbox.data;

//***************************

//***************************
// Thread B

mbox.data = some_very_important_value;
mbox.flag = 1;

//***************************

Мысль о том, что поток А будет вращаться в ожидании, пока mbox.flag покажет, что mbox.data содержит действительный фрагмент информации. Поток B запишет некоторые данные в mailbox.data, а затем установит для mbox.flag значение 1 в качестве сигнала о том, что mbox.data действителен.

В этом случае простое чтение в потоке A mbox.flag может получить значение 1, даже если последующее чтение mbox.data в потоке A не получит значение, записанное потоком B.

Это связано с тем, что даже если компилятор не будет переупорядочивать запись потока B в mbox.data и mbox.flag, процессор и / или кэш могут. C / C ++ гарантирует, что компилятор сгенерирует код таким образом, что поток B запишет в mbox.data до того, как он запишет в mbox.flag, но процессор и кэш могут иметь другое представление - специальная обработка, называемая «барьеры памяти» или «приобретение и семантика релиза »должна использоваться для обеспечения порядка ниже уровня потока инструкций потока.

Я не уверен, что компиляторы, кроме MSVC, предъявляют какие-либо претензии относительно порядка ниже уровня инструкции. Однако MS гарантирует, что для MSVC достаточно volatile - MS указывает, что volatile записи имеют семантику выпуска, а volatile чтения приобретают семантику - хотя я не уверен, к какой версии MSVC это применимо - см. http://msdn.microsoft.com/en-us/library/12a04hfd.aspx?ppud=4.

Я также видел код, подобный описанному вами, который использует Interlocked APIs для выполнения простых операций чтения и записи в общие местоположения. Мое мнение по этому вопросу заключается в использовании взаимосвязанных API. Блокировка без связи между потоками полна очень трудных для понимания и тонких ловушек, и попытка сократить критический фрагмент кода, который может привести к очень сложной диагностике ошибки, не кажется мне хорошей идеей , Кроме того, используя Interlocked API, выкрикивает всем, кто поддерживает код: «Это доступ к данным, который необходимо совместно использовать или синхронизировать с чем-то другим - осторожно! ».

Также при использовании Interlocked API вы берете на себя специфику аппаратного обеспечения и компилятора - платформа гарантирует, что все эти вещи обрабатываются должным образом - не более чем интересно ...

Прочтите Статьи Херба Саттера об эффективном параллелизме о DDJ (которые сейчас не работают, по крайней мере для меня) для получения полезной информации по этой теме.

6 голосов
/ 14 ноября 2013

Твой путь хорош:

LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);

Я использую аналогичное решение:

LONG Cur = InterlockedExchangeAdd(&_ServerState, 0);
5 голосов
/ 23 апреля 2009

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

1 голос
/ 08 августа 2016

Любому, кто должен вернуться к этой теме, я хочу добавить к тому, что было хорошо объяснено Бартошем, что _InterlockedCompareExchange() является хорошей альтернативой стандартному atomic_load(), если стандартная атомика недоступна. Вот код для атомарного чтения my_uint32_t_var в C на i86 Win64. atomic_load() включено в качестве эталона:

 long debug_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var);
00000001401A6955  mov         eax,dword ptr [rbp+30h] 
00000001401A6958  xor         edi,edi 
00000001401A695A  mov         dword ptr [rbp-0Ch],eax 
    debug_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0);
00000001401A695D  xor         eax,eax 
00000001401A695F  lock cmpxchg dword ptr [rbp+30h],edi 
00000001401A6964  mov         dword ptr [rbp-0Ch],eax 
    debug_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0);
00000001401A6967  prefetchw   [rbp+30h] 
00000001401A696B  mov         eax,dword ptr [rbp+30h] 
00000001401A696E  xchg        ax,ax 
00000001401A6970  mov         ecx,eax 
00000001401A6972  lock cmpxchg dword ptr [rbp+30h],ecx 
00000001401A6977  jne         foo+30h (01401A6970h) 
00000001401A6979  mov         dword ptr [rbp-0Ch],eax 

    long release_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var);
00000001401A6955  mov         eax,dword ptr [rbp+30h] 
    release_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0);
00000001401A6958  mov         dword ptr [rbp-0Ch],eax 
00000001401A695B  xor         edi,edi 
00000001401A695D  mov         eax,dword ptr [rbp-0Ch] 
00000001401A6960  xor         eax,eax 
00000001401A6962  lock cmpxchg dword ptr [rbp+30h],edi 
00000001401A6967  mov         dword ptr [rbp-0Ch],eax 
    release_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0);
00000001401A696A  prefetchw   [rbp+30h] 
00000001401A696E  mov         eax,dword ptr [rbp+30h] 
00000001401A6971  mov         ecx,eax 
00000001401A6973  lock cmpxchg dword ptr [rbp+30h],ecx 
00000001401A6978  jne         foo+31h (01401A6971h) 
00000001401A697A  mov         dword ptr [rbp-0Ch],eax
1 голос
/ 27 июля 2009

32-битные операции чтения уже атомарны на некоторых 32-битных системах (спецификация Intel говорит, что эти операции атомарны, но нет никакой гарантии, что это будет верно для других x86-совместимых платформ). Так что вы не должны использовать это для синхронизации потоков.

Если вам нужен какой-нибудь флаг, вам следует рассмотреть возможность использования Event объекта и WaitForSingleObject функции для этой цели.

0 голосов
/ 10 мая 2012

Ваше первоначальное понимание в основном правильно. В соответствии с моделью памяти, которая требуется Windows на всех платформах MP, которые она поддерживает (или когда-либо будет поддерживать), чтения из естественно выровненной переменной, помеченной как volatile, являются атомарными, если они меньше размера машинного слова. То же самое с пишет. Вам не нужен префикс 'lock'.

Если вы выполняете чтение без использования блокировки, вы можете изменить порядок процессора. Это может произойти даже на x86 в ограниченных обстоятельствах: чтение из переменной может быть перенесено над записью другой переменной. Практически во всех архитектурах, отличных от x86, которые поддерживает Windows, вы подвергаетесь еще более сложному переупорядочению, если не используете явные блокировки.

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

long g_var = 0;  // not marked 'volatile' -- this is an error

bool foo () {
    long oldValue;
    long newValue;
    long retValue;

    // (1) Capture the original global value
    oldValue = g_var;

    // (2) Compute a new value based on the old value
    newValue = SomeTransformation(oldValue);

    // (3) Store the new value if the global value is equal to old?
    retValue = InterlockedCompareExchange(&g_var,
                                          newValue,
                                          oldValue);

    if (retValue == oldValue) {
        return true;
    }

    return false;
}

Что может пойти не так, так это то, что компилятор имеет все права для повторного извлечения oldValue из g_var в любое время, если он не является изменчивым. Эта оптимизация «повторной материализации» очень полезна во многих случаях, поскольку она позволяет избежать проливания регистров в стек при высоком давлении в регистрах.

Таким образом, шаг (3) функции станет:

// (3) Incorrectly store new value regardless of whether the global
//     is equal to old.
retValue = InterlockedCompareExchange(&g_var,
                                      newValue,
                                      g_var);
0 голосов
/ 07 июля 2011

Чтение в порядке. 32-битное значение всегда читается целиком, если оно не разбито на строку кэша. Выравнивание 8 гарантирует, что оно всегда в пределах строки кэша, так что все будет в порядке.

Забудьте о переупорядочении инструкций и всем этом бессмысленном. Результаты всегда удаляются по порядку. В противном случае это будет отзыв процессора !!!

Даже для двухпроцессорной машины (то есть совместно используемой через самые медленные FSB) у вас все будет в порядке, поскольку процессоры гарантируют согласованность кэша через протокол MESI. Единственное, что вам не гарантировано, так это то, что прочитанное вами значение может быть не самым последним. НО, что является последним? Это то, что вам, вероятно, не нужно будет знать в большинстве ситуаций, если вы не пишете обратно в местоположение, основываясь на значении этого чтения. В противном случае вы бы использовали блокированные операции, чтобы справиться с этим.

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

EDIT: в ответ на комментарий, оставленный Adrian McCarthy .

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

Я не говорил, что чтение из энергонезависимой переменной - это нормально. Весь вопрос был в том, требуется ли блокировка. Фактически, рассматриваемая переменная была четко объявлена ​​с volatile. Или вы пропустили эффект ключевого слова volatile?

0 голосов
/ 23 апреля 2009

Функции Interlocked * предотвращают доступ двух разных процессоров к одному и тому же фрагменту памяти. В однопроцессорной системе вы будете в порядке. Если у вас двухъядерная система, в которой потоки на разных ядрах имеют доступ к этому значению, у вас могут возникнуть проблемы с выполнением атомарного режима без Interlocked *.

0 голосов
/ 23 апреля 2009

Для считывания текущего значения блокировка может не потребоваться.

0 голосов
/ 23 апреля 2009

ты должен быть в порядке. Он нестабилен, поэтому оптимизатор не должен вас дикари, и это 32-битное значение, поэтому оно должно быть хотя бы приблизительно атомарным. Одним из возможных сюрпризов является то, что конвейер команд может обойти это.

С другой стороны, какова дополнительная стоимость использования охраняемых подпрограмм?

...