2016-06-27 5 views
4

Если мы имеем следующий код C#:Защищают ли барьеры памяти свежие чтения на C#?

int a = 0; 
int b = 0; 

void A() // runs in thread A 
{ 
    a = 1; 
    Thread.MemoryBarrier(); 
    Console.WriteLine(b); 
} 

void B() // runs in thread B 
{ 
    b = 1; 
    Thread.MemoryBarrier(); 
    Console.WriteLine(a); 
} 

MemoryBarriers убедитесь, что команда записи происходит до чтения. Тем не менее, гарантировано ли, что запись одного потока видна чтением на другом потоке? Другими словами, гарантируется ли, что по крайней мере один поток печатает 1 или оба потока могут печатать 0?

Я знаю, что уже существует несколько вопросов, имеющих отношение к «свежести» и MemoryBarrier в C#, например this и this. Однако большинство из них имеют дело с шаблоном записи-освобождения и чтения. Код, размещенный в этом вопросе, очень специфичен для того, чтобы гарантировать, что запись гарантированно будет просматриваться путем чтения поверх того, что инструкции сохранены в порядке.

+0

Возможный дубликат [Что такое забор памяти? ] (http://stackoverflow.com/questions/286629/what-is-a-memory-fence) – nothrow

+2

@Yossarian это не дубликат по причинам, которые я объясняю. – Petrakeas

ответ

3

Не гарантировано видно, что обе потоки пишут 1. It only guarantees the order of read/write operations на основе этого правила:

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

Так что это в основном означает, что нить для thread A не будет использовать значение переменной b читать до того вызова шлагбаума. Но он по-прежнему кэшировать значение, если ваш код что-то вроде этого:

void A() // runs in thread A 
{ 
    a = 1; 
    Thread.MemoryBarrier(); 
    // b may be cached here 
    // some work here 
    // b is changed by other thread 
    // old value of b is being written 
    Console.WriteLine(b); 
} 

В гонке-состояние ошибки для а параллельное выполнение очень трудно воспроизвести, поэтому я не могу предоставить вам код, который, безусловно, сделать выше сценарий, но я предлагаю вам использовать volatile keyword для переменных, используемых разными потоками, как это работает точно так же, как вы хотите - дает вам свежий прочитанный для переменной:

volatile int a = 0; 
volatile int b = 0; 

void A() // runs in thread A 
{ 
    a = 1; 
    Thread.MemoryBarrier(); 
    Console.WriteLine(b); 
} 

void B() // runs in thread B 
{ 
    b = 1; 
    Thread.MemoryBarrier(); 
    Console.WriteLine(a); 
} 
+0

Вы уверены, что волатильность гарантирует свежее чтение? Документация может сказать что-то подобное, но прочитав несколько статей, и я не уверен в этом. Я знаю, что volatile отключает некоторые оптимизации и устраняет барьеры памяти, но я не уверен, что он гарантирует «свежие» чтения, как в этом случае. – Petrakeas

+0

Да, я уверен, что это гарантия, как сказано в документах. Не могли бы вы предоставить некоторые ссылки для этого? Вы также можете использовать метод «VolitileRead» из Interlocked, но это то же самое, что маркировка поля как «volatile» – VMAtm

+1

В этой ссылке указано, что для защиты памяти достаточно одного: https://msdn.microsoft.com/en- us/magazine/jj883956.aspx Этот вопрос относится к летучим и свежим: http://stackoverflow.com/questions/21652938/where-to-places-fences-memory-barriers-to-guarantee-a-fresh- read-commit-write – Petrakeas

3

это зависит от того, что означает «свежий». Thread.MemoryBarrier заставит первое считывание переменной получить, загрузив ее из ее назначенной ячейки памяти. Если это все, что вы имеете в виду под «свежим» и не более чем, то ответ «да». Большинство программистов работают с более жестким определением, понимают ли они это или нет, и именно там начинаются проблемы и путаница. Обратите внимание, что волатильное чтение через volatile и другие аналогичные механизмы не производят «свежие» данные под этим определением, но под другим определением. Продолжайте читать, чтобы узнать, как это сделать.

Я буду использовать стрелку вниз ↓ для представления изменчивого считывания и стрелки вверх ↑ для представления волатильной записи. Подумайте о том, что голова стрелки отталкивает другие чтения и записи. Код, который генерирует эти заграждения памяти, может свободно перемещаться, пока никакая команда не продвигается по стрелке вниз и вниз по стрелке вверх. Запоминания памяти (стрелки), однако, заблокированы на месте в месте, где они были первоначально объявлены в коде. Thread.MemoryBarrier создает барьер с полным заграждением, поэтому он имеет как семантику считывания, так и освобождение-запись.

int a = 0; 
int b = 0; 

void A() // runs in thread A 
{ 
    register = 1 
    a = register 
    ↑ // Thread.MemoryBarrier 
    ↓ // Thread.MemoryBarrier 
    register = b 
    jump Console.WriteLine 
    use register 
    return Console.WriteLine 
} 

void B() // runs in thread B 
{ 
    register = 1 
    b = register 
    ↑ // Thread.MemoryBarrier 
    ↓ // Thread.MemoryBarrier 
    register = a 
    jump Console.WriteLine 
    use register 
    return Console.WriteLine 
} 

Имейте в виду, что линии C# на самом деле являются множественными инструкциями после их компиляции и выполнения JIT. Я попытался проиллюстрировать это несколько, но на самом деле вызов Console.WriteLine по-прежнему будет намного сложнее, чем показано, поэтому время между чтением a или b и их первое использование может быть значительным относительно. Поскольку Thread.MemoryBarrier создает забор, считываниям не разрешается всплывать и проходить вызов. Таким образом, чтение является «свежим» относительно вызова Thread.MemoryBarrier. Но он может быть «устаревшим» относительно того, когда он фактически используется вызовом Console.WriteLine.

Давайте рассмотрим, как выглядит ваш код, если мы заменим вызов Thread.MemoryBarrier ключевым словом volatile.

volatile int a = 0; 
volatile int b = 0; 

void A() // runs in thread A 
{ 
    register = 1 
    ↑    // volatile write 
    a = register 
    register = b 
    ↓    // volatile read 
    jump Console.WriteLine 
    use register 
    return Console.WriteLine 
} 

void B() // runs in thread B 
{ 
    register = 1 
    ↑    // volatile write 
    b = register 
    register = a 
    ↓    // volatile read 
    jump Console.WriteLine 
    use register 
    return Console.WriteLine 
} 

Можете ли вы определить изменение? Если вы моргнули, вы пропустили его. Сравните расположение стрелок (заграждений памяти) между двумя блоками кода. В первом случае (Thread.MemoryBarrier) считывание не допускается в момент времени до барьера памяти. Но во втором случае (volatile) чтение может пузыриться бесконечно (потому что есть стрелка вниз, отталкивающая их). В этом случае можно сделать разумный аргумент, согласно которому Thread.MemoryBarrier может производить «свежее» чтение, если оно помещено перед чтением, чем решение volatile. Но, можете ли вы по-прежнему утверждать, что чтение «свежее»? Не совсем потому, что к моменту его использования Console.WriteLine это может быть не последнее значение.

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

volatile int a = 0; 

void A() 
{ 
    register = a; 
    ↓    // volatile read 
    Console.WriteLine(register); 
    register = a; 
    ↓    // volatile read 
    Console.WriteLine(register); 
    register = a; 
    ↓    // volatile read 
    Console.WriteLine(register); 
} 

Обратите внимание на то, что может случиться здесь. Показаны строки register = a. Обратите внимание, где находится стрелка ↓. Поскольку он помещается после чтения, нет ничего, что предотвращало бы плавание вверх. Он может фактически всплывать и до предыдущего вызова Console.WriteLine. Таким образом, в этом случае нет гарантии, что Console.WriteLine работает с последним значением a. Тем не менее, гарантируется, что он будет работать с более новым значением, чем в последний раз, когда он был вызван. Это его полезность в двух словах. Вот почему вы видите много незакрепленного кода, вращающегося в цикле while, чтобы предыдущее чтение изменчивой переменной равно текущему чтению, прежде чем предполагать, что его предполагаемая операция прошла успешно.

Есть несколько важных моментов, которые я хочу сделать в заключение.

  • Thread.MemoryBarrier гарантирует, что чтение появляется после того, как он возвращает последнее значение по отношению к барьеру. Но к тому времени, когда вы на самом деле принимаете решения или используете эту информацию, она больше не может быть последней ценностью.
  • volatile гарантирует, что чтение вернет значение, которое является новее, чем предыдущее чтение той же переменной. Ни в коем случае это не гарантирует, что значение является последним, хотя.
  • Значение «свежего» должно быть четко определено, но может отличаться от ситуации к ситуации и разработчика для разработчика. Нет никакого смысла, который будет более правильным, чем что-либо другое, если оно может быть формально определено и сформулировано.
  • Это не абсолютная концепция. Вы найдете более полезным определить «свежие» с точки зрения того, чтобы быть относительно чего-то другого, например, создания барьера памяти или предыдущей инструкции. Другими словами, «свежесть» - это относительное понятие, как скорости относительно наблюдателя в теории специальной теории относительности Эйнштейна.
+0

Брайан, ваше представление стрелки (существующее в других ваших SO-ответах) действительно полезно. «Свежесть» действительно является неопределенным термином, поэтому этот вопрос сосредоточен на этом конкретном примере. Как вы отмечаете, семантика read-приобретать/писать-релиз (применяемая volatile) недостаточно для этого примера. Этот пример требует, чтобы загрузка магазина не была перенаправлена, и это гарантируется только полным барьером. Некоторые ссылки, которые помогли мне (даже если они предназначены для C++): http://preshing.com/20120515/memory-reordering-caught-in-the-act/ http://preshing.com/20120913/acquire- и-release-семантика/ – Petrakeas

+0

«Thread.MemoryBarrier генерирует барьер с полным заграждением, поэтому он имеет как семантику чтения, так и освобождение-запись». Это не совсем правильно, хотя я понимаю, что вы имеете в виду. Барьер Full-Fence - это больше, чем чтение-запись и запись-релиз. Он запрещает повторный заказ на загрузку магазина, что не делается ни тем, ни другим. Именно поэтому он реализован с использованием более дорогостоящего вызова процессора. – Petrakeas

1

Приведенные выше ответы в основном верны. Тем не менее, чтобы дать более краткий пояснения к вашему вопросу: «Гарантировано ли, что хотя бы один поток печатает 1?» - Да, пара барьеров памяти гарантирует это.

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

Если методы A и B называются точно то же самое время, вы могли бы получить два 1s:

| Thread A | Thread B | 
|    |    | 
| a = 1  | b = 1  | 
| ------------ | ------------ | 
| read b | read a | 
|    |    | 

Однако, вероятность того, что они будут вызываться друг от друга, давая 0 и 1:

| Thread A | Thread B | 
|    |    | 
| a = 1  |    | 
| ------------ |    | 
| read b |    | 
|    |    | 
|    | b = 1  | 
|    | ------------ | 
|    | read a | 

переназначение памяти может привести чтение и/или запись на одной переменных, которые будет сдвинуто за пределами друг друга, снова вызывая два 1s:

| Thread A | Thread B | 
|    |    | 
| a = 1  |    | 
| ------------ |    | 
|    | b = 1  | 
|    |    | 
| read b |    | 
|    | ------------ | 
|    | read a | 

Однако, нет никакого способа, которым вы могли бы получить чтения и/или запись из как переменных, которые будут сдвинут за пределами друг друга, так как барьеры запрещают это. Поэтому невозможно получить два 0s.

Возьмем второго примера выше, где b был прочитан как 0. К тому времени b считывали на нить А, a бы уже был написан как 1 и сделан видимым для других потоков, из-за барьер памяти на нити A. Однако a не мог быть прочитан или сохранен в потоке в потоке B, поскольку барьер памяти на резьбе B еще не достигнут, поскольку b по-прежнему равен 0.

Смежные вопросы