2013-10-15 5 views
27

Я использовал volatile, где я не уверен, что это необходимо. Я был уверен, что замок в моей ситуации будет излишним. Чтение этой темы (комментарий Эрика Липперта) заставляет меня беспокоиться о моем использовании волатильности: When should the volatile keyword be used in c# ?C# - Использование летучих ключевых слов против блокировки

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

Я добавил «volatile», чтобы убедиться, что не происходит выравнивания пропусков: чтение только 32 бит переменной и других 32 бит на другую выборку, которая может быть разбита на 2 путем записи в середине из другого потока ,

Может ли мое предыдущее предположение (предыдущее утверждение) действительно произойти? Если нет, используется ли «изменчивое» использование (модификации свойств свойства могут произойти в любом потоке).

После прочтения 2 первых ответов. Я хотел бы настаивать на том, что способ написания кода неважно, если из-за параллелизма мы пропустим инкремент (хотим увеличивать с 2 потоков, но результат увеличивается только на один из-за параллелизма), если хотя бы переменная «_actualVersion» увеличивается.

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

public abstract class OptionsBase : NotifyPropertyChangedBase 
{ 
    private string _path; 

    volatile private int _savedVersion = 0; 
    volatile private int _actualVersion = 0; 

    // ****************************************************************** 
    void OptionsBase_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) 
    { 
     _actualVersion++; 
     Application.Current.Dispatcher.BeginInvoke(new Action(InternalSave), DispatcherPriority.ApplicationIdle); 
    } 

    // ****************************************************************** 
    private void InternalSave() 
    { 
     if (_actualVersion != _savedVersion) 
     { 
      _savedVersion = _actualVersion; 
      Save(); 
     } 
    } 

    // ****************************************************************** 
    /// <summary> 
    /// Save Options 
    /// </summary> 
    private void Save() 
    { 
     using (XmlTextWriter writer = new XmlTextWriter(_path, null)) 
     { 
      writer.Formatting = Formatting.Indented; 
      XmlSerializer x = new XmlSerializer(this.GetType()); 

      x.Serialize(writer, this); 
      writer.Close(); 
     } 
    } 
+0

Не то, что вы просили (поэтому добавили в качестве комментария), но я бы, вероятно, переместил вызов метода Save() на строку '_savedVersion = _actualVersion '. Таким образом, если Save() генерирует исключение, переменная _savedVersion не будет обновляться некорректно. – Baldrick

+0

Почему вы сохраняете из потока 'Dispatcher' событие ?! –

+0

@ Baldrick, я не могу делать то, что вы говорите (перемещение) из-за многопоточной импликации (я могу пропустить изменение версии, если я это сделаю). Но вы отчасти правы, где я должен предотвратить исключение моего кода. Благодаря ! –

ответ

57

Я использовал volatile, где я не уверен, что это необходимо.

Позвольте мне быть очень ясно по этому вопросу:

Если вы не 100% ясно, что летучие средства в C#, то не использовать его. Это острый инструмент, предназначенный только для экспертов. Если вы не можете описать, что все возможные переупорядочивания доступа к памяти разрешены архитектурой модели слабой памяти, когда два потока читают и записывают два разных изменчивых поля, тогда вы не знаете достаточно, чтобы безопасно использовать волатильность, и вы будете совершать ошибки, поскольку у вас есть и написать программу, которая чрезвычайно хрупка.

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

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

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

Цена за фиксированный замок, конечно, сколь угодно высока, если код внутри замка дорог. Не делайте дорогой работы внутри замка, так что вероятность спора низкая.

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

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

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

Ваше предложение бессмысленна, потому что:

Во-первых, целые числа уже только 32 бита.

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

В-третьих, да, это правда, что волатильные обращения всегда являются атомарными, но это не потому, что C# делает все изменчивые обращения доступными для атомарного доступа! Скорее, C# делает незаконным ставить volatile в поле, если поле не является уже атомарным.

В-четвертых, целью волатильности является предотвращение компилятора C#, дрожания и процессора от определенных оптимизаций, которые могут изменить смысл вашей программы в модели с слабой памятью. Летучий, в частности, не делает ++ атомарным. (Я работаю в компании, которая делает статические анализаторы, я буду использовать ваш код в качестве тестового примера для нашей «неправильной неатомной операции на летучем поле». Мне очень полезно получить реальный код, полный реалистичные ошибки, мы хотим убедиться, что мы на самом деле находим ошибки, которые люди пишут, поэтому благодарим за публикацию этого.)

Рассматривая ваш фактический код: изменчивый, как указал Ханс, совершенно неадекватно, чтобы сделать ваш код верный. Лучше всего сделать то, что я сказал ранее: не позволяют эти методы вызываться в любом потоке, отличном от основного потока. То, что логика счетчика неверно, должно быть наименьшим из ваших забот. Что делает поток сериализации безопасным, если код другого потока изменяет поля объекта во время его сериализации? Это проблема, о которой вы должны сначала беспокоиться.

+0

Спасибо Эрик. У меня нет намерения продолжать использовать «volatile» (пока я не знаю точно, что это значит), но я продолжу писать MT-код, хотя вы, кажется, думаете, что я не должен ;-)! Я все еще задаюсь вопросом, является ли 64-битное целое число write или fetch атомарным на 32-битной и 64-битной (смещенной на 64-битной) системе? –

+0

@EricOuellet: в соответствии с документацией для [Interlocked.Read] (http://msdn.microsoft.com/en-us/library/system.threading.interlocked.read.aspx): «В 32-разрядных системах 64 -битные операции чтения не являются атомарными, если не выполняются с использованием Read. " 64-разрядные записи также не являются атомарными в 32-битных системах. –

+0

@ Jim, Спасибо! Я этого не знал. Я очень рад узнать. –

0

Сравнение и назначение в методе InternalSave() не было бы безопасным потоком с ключевым словом volatile или без него. Если вы хотите избежать использования блокировок, вы можете использовать методы CompareExchange() и Increment() в своем коде из класса Interlocked.

+0

'Interlocked.Increment' будет хорошо работать для приращения, но' CompareExchange' не будет обрабатывать условие гонки в методе 'InternalSave'. Похоже, ему нужен замок. –

+0

Условие гонки здесь не является проблемой из-за способа написания кода. Но, как говорит Ханс, у меня могут быть некогерентные сохраненные данные, если я не использую блокировку для предотвращения сохранения, пока я изменяю свойства из других потоков. –

+0

@Jim. Сохранение может происходить только в потоке пользовательского интерфейса (только один поток) и по умолчанию безопасно. Если вы говорили о связи между другим кодом и сохранением, то да, вы были правы, и мне нужен замок. Спасибо, Эрик. –

1

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

До тех пор, пока вы используете Int32, у вас нет проблемы с тем, что вы заявляете.

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

EDIT

Рассматривали ли вы с помощью Interlocked.Increment?

+0

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

13

Неустойчивый крайне неадекватен, чтобы сделать этот код безопасной. Вы можете использовать блокировку низкого уровня с помощью Interlocked.Increment() и Interlocked.CompareExchange(), но есть очень мало оснований предполагать, что Save() является потокобезопасным. Кажется, он пытается сохранить объект, который изменяется рабочим потоком.

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

+0

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

3

В соответствии с превосходным post on threads and locks Джо Албахари, который из его одинаково отличной книги C# 5.0 В двух словах он говорит here, что даже когда используется ключевое слово volatile, запись записи, за которой следует инструкция чтения, может быть переупорядочена.

Далее, он говорит, что документация по MSDN по этой теме неверна и предполагает, что для существует сильный случай, позволяющий избежать ключевого слова volatile. Он указывает, что, даже если вы понимаете тонкости, будут ли другие разработчики на линии также понимать это?

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

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