Могу ли я избежать использования блокировок для моей редко меняющейся переменной? - PullRequest
11 голосов
/ 29 января 2010

Я читал книгу Джо Даффи о параллельном программировании. У меня есть академический вопрос о потоке без блокировки.

Во-первых: я знаю, что создание потоков без блокировки чревато опасностью (если вы мне не верите, прочитайте разделы книги о модели памяти)

Тем не менее, у меня есть вопрос: Предположим, у меня есть класс со свойством int.

Значение, на которое ссылается это свойство, будет очень часто читаться несколькими потоками

Крайне редко это значение будет меняться, и когда это произойдет, это будет один поток, который его изменит.

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

Я мог бы использовать блокировки (или readloerlockslim для обеспечения одновременного чтения). Я мог бы пометить переменную volatile (множество примеров, где это делается)

Тем не менее, даже изменчивый может наложить производительность производительности.

Что, если я использую VolatileWrite, когда он изменяется, и оставляю доступ нормальным для чтения. Примерно так:

public class MyClass
{
  private int _TheProperty;
  internal int TheProperty
  {
    get { return _TheProperty; }
    set { System.Threading.Thread.VolatileWrite(ref _TheProperty, value); }
  }
}

Я не думаю, что когда-нибудь попробую это в реальной жизни, но мне любопытно узнать ответ (более всего, в качестве контрольной точки того, понимаю ли я материал модели памяти, который читал). 1020 *

Ответы [ 7 ]

6 голосов
/ 29 января 2010

Пометка переменной как "volatile" имеет два эффекта.

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

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

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

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

while(this.prop == 0) {}

джиттер имеет право создавать этот код, как если бы вы написали

if (this.prop == 0) { while (true) {} }

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

4 голосов
/ 29 января 2010

Вопрос в том, увидит ли ветка чтения когда-либо изменения. Вопрос не только в том, видит ли он это немедленно .

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

2 голосов
/ 29 января 2010

На уровне ЦП, да, каждый процессор в конечном итоге увидит изменение адреса памяти. Даже без замков и барьеров памяти. Блокировки и барьеры просто гарантировали бы, что все это происходило в относительном порядке (с другими инструкциями), так что это казалось правильным для вашей программы.

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

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

Или, эквивалентно, он был написан позже, чем логика вашего кода думала, что он будет написан.

Или оба.

В любом случае, если бы это был C / C ++, даже без блокировок / барьеров, вы бы в итоге получили бы обновленные значения. (обычно в течение нескольких сотен циклов, так как память занимает столько же времени). В C / C ++ вы можете использовать volatile (слабое непотоковое volatile), чтобы гарантировать, что значение не будет считано из регистра. (Теперь есть некогерентный кеш! Т.е. регистры)

В C # я недостаточно знаю о CLR, чтобы знать, как долго значение может оставаться в регистре, или как гарантировать, что вы действительно перечитываете из памяти. Вы потеряли «слабую» волатильность.

Я подозреваю, что до тех пор, пока доступ к переменным не будет полностью скомпилирован, у вас в конечном итоге закончатся регистры (у x86 не так много для начала), и вы перечитаете.

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

2 голосов
/ 29 января 2010

«Повышение производительности» volatile связано с тем, что компилятор теперь генерирует код для фактической проверки значения, а не для его оптимизации - другими словами, у вас будет , чтобы получить этот показатель производительности независимо от того, о том, что вы делаете.

1 голос
/ 29 января 2010

Для обычных вещей (например, для устройств с отображенной памятью) существуют протоколы когерентности кэша, которые выполняются внутри / между ЦП / ЦП, чтобы гарантировать, что разные потоки, совместно использующие эту память, получают согласованное представление о вещах (то есть, если я изменяю значение ячейки памяти в одном процессоре, оно будет видно другим процессорам, которые имеют память в своих кэшах). В этом отношении volatile поможет гарантировать, что оптимизатор не оптимизирует доступ к памяти (который всегда проходит через кеш), например, путем чтения значения, кэшированного в регистре. Документация C # кажется довольно ясной по этому вопросу. Опять же, прикладному программисту обычно не приходится иметь дело с когерентностью кэша.

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

1 голос
/ 29 января 2010

Это шаблон, который я использую, когда шаблон «последний писатель выигрывает» применим к ситуации. Я использовал ключевое слово volatile, но, увидев этот шаблон в примере кода Джеффри Рихтера, я начал его использовать.

0 голосов
/ 29 января 2010

В C # тип int является потокобезопасным.

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

Однако вы можете объявить ее volatile , если обновление ОС будет выполнять обновление ОС.

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

a = !a;

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

...