Подтвердить что ты не робот

Чтение взаимосвязанных переменных

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

а. С++ под WIN32.

В. Правильно выровненное неизменяемое целое число увеличивается и уменьшается с помощью 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
}

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

Мэтт Дж.


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

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

LONG x = InterlockedExchange(&_ServerState, _ServerState);

Это имеет побочный эффект изменения значения, когда все, что я действительно хочу сделать, это прочитать его. Не только это, но есть вероятность, что я могу reset флаг ошибочного значения, если есть переключатель контекста, поскольку значение _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);
4b9b3361

Ответ 1

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

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

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

Ничто из этого не обещано стандартом 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 действителен.

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

Это связано с тем, что, несмотря на то, что компилятор не будет изменять порядок записи потока B в mbox.data и mbox.flag, процессор и/или кеш могут. C/С++ гарантирует, что компилятор будет генерировать код таким образом, что Thread B будет записывать в mbox.data до его записи в mbox.flag, но процессор и кеш могут иметь другую идею - специальную обработку, называемую "барьерами памяти" или "приобретать и Release semantics 'необходимо использовать для обеспечения порядка ниже уровня потока потоков инструкций.

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

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

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

Прочтите Herb Sutter Эффективные статьи Concurrency на DDJ (которые, по крайней мере, для меня сейчас недоступны) для хорошей информации на эту тему.

Ответ 2

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

Ответ 3

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

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

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

LONG Cur = InterlockedExchangeAdd(&_ServerState, 0);

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

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

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

EDIT: В ответ на комментарий, оставленный Адрианом Маккарти.

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

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

Ответ 9

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

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

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

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

Ответ 10

Для тех, кто должен пересмотреть этот поток, я хочу добавить к тому, что хорошо объяснил Бартош, что _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