Гарантия актуальности переменной в .NET (изменчивое или изменчивое чтение)

Я прочитал много противоречивой информации (msdn, SO и т. Д.) О volatile и VoletileRead (ReadAcquireFence).

Я понимаю последствия этого ограничения переупорядочения доступа к памяти - что меня до сих пор полностью смущает, так это гарантия свежести, что для меня очень важно.

В msdn doc for volatile упоминается:

(...) Это гарантирует, что самое последнее значение всегда присутствует в поле.

msdn doc для изменчивых полей упоминает:

Чтение изменчивого поля называется изменчивым чтением. Неустойчивое чтение имеет «семантику получения»; то есть гарантируется, что это произойдет до любых обращений к памяти, которые происходят после него в последовательности команд.

. NET-код для VolatileRead:

public static int VolatileRead(ref int address)
{
    int ret = address;
    MemoryBarrier(); // Call MemoryBarrier to ensure the proper semantic in a portable way.
    return ret;
}

Согласно msdn MemoryBarrier doc Барьер памяти предотвращает переупорядочивание. Однако, похоже, это не влияет на свежесть - верно?

Как же тогда получить гарантию свежести? И есть ли разница между маркировкой поля volatile и доступом к нему с помощью семантики VolatileRead и VolatileWrite? В настоящее время я делаю последний критический для производительности код, который должен гарантировать свежесть, однако читатели иногда получают устаревшее значение. Мне интересно, изменит ли ситуацию маркировка состояния volatile.

РЕДАКТИРОВАТЬ1:

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

Если у изменчивой конструкции или конструкции более высокого уровня (например, блокировки) есть эта гарантия (есть ли?), То как они этого достигают?

РЕДАКТИРОВАТЬ2:

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

Из того, что я здесь узнал, мне интересно, может ли это быть решением (строка решения (?) Отмечена комментарием):

private SharedState _sharedState;
private SpinLock _spinLock = new SpinLock(false);

public void Update(SharedState newValue)
{
    bool lockTaken = false;
    _spinLock.Enter(ref lockTaken);

    _sharedState = newValue;

    if (lockTaken)
    {
        _spinLock.Exit();
    }
}

public SharedState GetFreshSharedState
{
    get
    {
        Thread.MemoryBarrier(); // <---- This is added to give readers freshness guarantee
        var value = _sharedState;
        Thread.MemoryBarrier();
        return value;
    }
}

Вызов MemoryBarrier был добавлен, чтобы убедиться, что и чтение, и запись - закрыты полными ограждениями (так же, как код блокировки - как указано здесь http://www.albahari.com/threading/part4.aspx#_The_volatile_keyword раздел« Барьеры памяти и блокировка »)

Выглядит правильно или ошибочно?

РЕДАКТИРОВАТЬ3:

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


person Jan    schedule 10.07.2014    source источник
comment
Вы знаете, что барьеры предотвращают повторный заказ, но не понимаете, как это подразумевает свежесть. Начните с тщательного определения того, что вы подразумеваете под свежестью.   -  person Eric Lippert    schedule 10.07.2014
comment
В более общем плане скажите, чего вы пытаетесь достичь. Неустойчивые поля - это инструменты очень низкого уровня. Вероятно, вам следует использовать инструмент более высокого уровня.   -  person Eric Lippert    schedule 10.07.2014
comment
@EricLippert Большое спасибо Эрику за участие. Я соответствующим образом отредактировал свой вопрос. Если более высокий инструмент (замок и т. Д.) Предлагает гарантию считывания как можно более свежего значения, как они этого достигают? У меня есть сотни миллионов операций записи в секунду, поэтому я хочу (и должен) точно понимать, как получить последнее записанное значение с минимально возможным количеством блокировок (поэтому я изначально предпочитал для блокировки изменчивые операции чтения / записи. А теперь исследую различия между энергозависимое чтение / запись, энергозависимая конструкция и конструкция блокировки - не считая монопольного доступа, просто кэширует когерентность памяти)   -  person Jan    schedule 10.07.2014
comment
Если у вас сотни миллионов операций записи в секунду, то у вас нет последнего значения, что бы вы ни делали. Предположим, вы читаете. Две инструкции выполняются в потоке чтения , и значение уже устарело.   -  person Eric Lippert    schedule 10.07.2014
comment
Я также отмечаю, что энергозависимое чтение одной переменной может быть переупорядочено относительно энергозависимой записи другой переменной; Изменчивость C # не означает, что существует единый правильный порядок всех изменчивых операций чтения и записи. Пример того, как это может вас укусить, можно найти на странице blog.coverity.com/ 2014/03/26 / reordering-optimizations   -  person Eric Lippert    schedule 10.07.2014
comment
Чтобы ответить на ваш последний вопрос: у блокировок действительно есть такая гарантия, и то, как они делают эту гарантию, является деталью реализации джиттера; техника варьируется от ЦП к ЦП. Обычно они снимают ограждения как для чтения, так и для записи - когда элемент управления входит и выходит из тела блокировки.   -  person Eric Lippert    schedule 10.07.2014
comment
@EricLippert: Ты прав, Эрик, что я не получу самую последнюю в то время, когда использую это значение. Однако это расследование было начато с бизнес-кейса, когда значение было прочитано более 70 мс. Меня устраивает отклонение в порядке переключения контекста или около того. Переупорядочивание инструкций не повредит нам - нам просто нужно как можно более свежее единственное значение и мы принимаем решения на основе этого. Но устаревшие решения нам сильно навредят.   -  person Jan    schedule 10.07.2014
comment
@EricLippert Спасибо за комментарий к блокировке! Правильно ли я прочитал, что и чтение, и запись общего состояния должны быть заключены в MemoryBarriers (до и после)? Однако, чем Thread.VolatileRead doc и volatile doc, скорее всего, ошибаются, поскольку они оба заявляют о гарантии считывания «наиболее актуального» значения, несмотря на то, что оба они используют только полузабор. Правильный?   -  person Jan    schedule 10.07.2014
comment
@EricLippert Также, если я правильно понимаю - простая маркировка изменчивого общего состояния не даст гарантии «свежести» (поскольку это всего лишь половина ограждения). Я публикую свое состояние в области SpinLock.Enter SpinLock.Exit (оба являются полным ограждением из-за использования Interlocked), поэтому добавление одного MemoryBarier перед чтением общего состояния (в коде VolatileRead, вставленном в мой вопрос) должно имитировать поведение, которое (обычно ) используется джиттером, чтобы гарантировать свежесть доступа в пределах области блокировки. Или я что-то упустил?   -  person Jan    schedule 10.07.2014
comment
Как называется GetFreshSharedState? Вызывается ли он повторно в цикле? Вы можете разместить код? Он не обязательно должен быть полным или что-то в этом роде, могут быть полезны лишь некоторые фрагменты того, как он используется.   -  person Brian Gideon    schedule 10.07.2014
comment
@BrianGideon Основной сценарий состоит в том, что около дюжины потоков считывают (очень часто) данные из сети, и после каждого чтения они принимают решение вызвать GetFreshSharedState. Обновление вызывается точно так же.   -  person Jan    schedule 11.07.2014
comment
Я не понимаю, почему вы считаете, что полузабор недостаточен для обеспечения того, что вы подразумеваете под свежестью. Возможно, это поможет: msdn. microsoft.com/en-us/library/windows/hardware/   -  person Eric Lippert    schedule 11.07.2014
comment
Я также отмечаю, что 70 мс - это меньше пяти квантов на машине с квантом потока 16 мс. Откуда вы знаете, что задержка в 70 мс не связана с четырьмя потоками, каждый из которых работает на процессоре между чтением из общей памяти и кодом, потребляющим это значение?   -  person Eric Lippert    schedule 11.07.2014
comment
@EricLippert Этот случай произошел на машине (x64) с разрешением часов ОС 1 мс, а также файл журнала (записанный из единой параллельной очереди с событиями, отмеченными атомными часами во время их вставки в очередь) показывает, что было довольно много активности других потоки писателей (включая выполнение Update) и поток сомнительных читателей между моментом времени, когда «устаревшее» значение было записано обновлением, и моментом времени, когда читатель прочитал его с помощью GetFreshSharedState.   -  person Jan    schedule 11.07.2014
comment
@EricLippert Это то, что заставило меня усомниться в полуфехтовании. А затем многочисленные примеры, такие как этот из C # 4 in Nutshell   -  person Jan    schedule 11.07.2014


Ответы (2)


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

Так что же означает, что поток все равно прочитает новое значение? Означает ли это, что значение, возвращаемое чтением, гарантированно не старше 100 мс, 50 ​​мс или 1 мс? Или это означает, что значение является самым последним? Или это означает, что если два чтения происходят подряд, то второе гарантированно получит более новое значение, если адрес памяти изменился после первого чтения? Или это вообще что-то значит?

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

public static T InterlockedOperation<T>(ref T location, T operand)
{
  T initial, computed;
  do
  {
    initial = location;
    computed = op(initial, operand); // where op is replaced with a specific implementation
  } 
  while (Interlocked.CompareExchange(ref location, computed, initial) != initial);
  return computed;
}

В приведенном выше коде мы можем создать любую взаимосвязанную операцию, если воспользуемся тем фактом, что второе чтение location через Interlocked.CompareExchange будет гарантированно возвращать более новое значение, если адрес памяти получил запись после первого читать. Это потому, что метод Interlocked.CompareExchange создает барьер памяти. Если значение изменилось между чтениями, тогда код будет повторяться по циклу до тех пор, пока location не перестанет изменяться. Этот шаблон не требует, чтобы код использовал последнее или самое свежее значение; просто более новое значение. Различие очень важно. 1

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

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

Если вы можете уменьшить значение слова «свежий» только до «более нового», то вызов Thread.MemoryBarrier после каждого чтения, использование Volatile.Read, использование ключевого слова volatile и т. Д. Будет абсолютно гарантировать, что одно чтение в последовательности вернет более новое значение, чем при предыдущем чтении.


1 Проблема ABA открывает новую банку червей .

person Brian Gideon    schedule 12.07.2014
comment
Спасибо, Брайан, что потратил на это время! Просто чтобы быстро определить, что я имею в виду под словом свежее: последнее записанное значение любым писателем (независимо от кеширования и т. Д.). Чем больше я читаю о барьерах, тем больше меня беспокоит, что они не определены для этого (они предназначены для предотвращения типов переупорядочивания), но, возможно, я просто ошибаюсь. Что очень хорошо, так это указание на Interlocked - эти операции по определению гарантированно получают самое свежее значение. Я прихожу к выводу, что мое свойство чтения должно просто вызывать return Interlocked.CompareExchange (ref _sharedState, null, null); - person Jan; 12.07.2014
comment
Да, Interlocked.CompareExchange вернет последнее значение. Но к тому времени, когда ваша логика использует это значение, оно может быть уже не последним. Именно это я имел в виду, когда предлагал попытаться сделать читателей менее зависимыми от фактической актуальности ценностей. - person Brian Gideon; 12.07.2014
comment
Спасибо, Брайан, твой вопрос в настоящее время лучше всего подходит к моему очень расплывчатому вопросу. Я хочу оставить обсуждение открытым на день или два, прежде чем отмечать. Однако я создал гораздо более конкретный вопрос (см. Edit3). Что касается требований к времени и свежести - это в основном статистика и `` неудачи '' - немного более медленное обновление кешей + несколько неудачных свопов (+ пропуски страниц или что-то еще), а немного более старое значение может внезапно в 1 из миллиардов случаев стать более старым значением - Мне нужно максимально исключить такую ​​возможность. - person Jan; 14.07.2014

Барьер памяти действительно дает эту гарантию. Мы можем получить свойство «свежести», которое вы ищете, из свойств изменения, которые гарантирует барьер.

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

Допустим, у нас есть эти операции, каждая в отдельном потоке:

x = 1
x = 2
print(x)

Как мы могли напечатать значение, отличное от 2? Без volatile чтение может переместиться на один слот вверх и вернуть 1. Однако Volatile предотвращает переупорядочивание. Запись не может двигаться назад во времени.

Короче говоря, volatile гарантирует, что вы увидите самое последнее значение.

Строго говоря, здесь мне нужно различать энергозависимость и барьер памяти. Последний вариант - более сильная гарантия. Я упростил это обсуждение, потому что энергозависимость реализована с использованием барьеров памяти, по крайней мере, на x86 / x64.

person usr    schedule 10.07.2014
comment
Спасибо за участие. Если 1 записано потоком A в 22:01, а 2 записано потоком B в 22:10 - гарантировано ли вам, что чтение, выполненное потоком C в 22:20, получит значение 2? Нет. Я не вижу этой гарантии ни в одной спецификации - я вижу только гарантию того, что вы увидите правильный снимок памяти во времени. - person Jan; 10.07.2014
comment
С другой стороны, Thread .VolatileRead MSDN doc упоминает Считывает значение поля. Это значение является последним, записанным любым процессором компьютера, независимо от количества процессоров или состояния кеш-памяти процессора. Однако иногда я наблюдаю противоречивое поведение в нашем наборе инструментов. И я могу найти множество противоречивой информации в Интернете (например, два комментария, указанные выше). В замешательстве: | - person Jan; 10.07.2014
comment
@Jan действительно читает, может переупорядочивать указанные выше записи в соответствии с albahari.com/threading/part4.aspx section Ключевое слово volatile. Это неправильный ответ, вскоре он будет удален. Ответ, который вы ищете: нет, вам не гарантируется последняя стоимость. - person usr; 10.07.2014
comment
Эта тема очень запутанная. Для других читателей может быть полезно, если вы оставите свой ответ (который обычно считается ожидаемым поведением) и просто отредактируете его, чтобы указать, что он неправильный. - person Jan; 10.07.2014
comment
@Jan Я согласен, но мне все равно, чтобы его переписать ... Опубликуйте свой ответ, и я проголосую за него. - person usr; 10.07.2014
comment
Судя по тому, что я увидел в нашем наборе инструментов, я ожидал этого: | Это послужило причиной возникновения этого вопроса. Скорее надо было сказать: как получить гарантию свежести? - person Jan; 10.07.2014
comment
@ Янв, верно. Задайте новый вопрос в дополнение к этому. - person usr; 10.07.2014
comment
Кстати, я не совсем уверен, что задержка в 70 мс, которую вы видите, связана с этим переупорядочением. x86 имеет тенденцию быть очень когерентным с памятью. Вероятно, ваш читатель прочитал очень актуальное значение, а затем ОС отключила его на 70 мс. Или произошло несколько ошибок страниц. Вы ничего не можете с этим поделать, чтобы исправить это всегда. - person usr; 10.07.2014