c# - использование ключевых слов Volatile vs lock

я использовал volatile, где я не уверен, что это необходимо. Я был уверен, что в моей ситуации замок был бы излишним. Чтение этой темы (комментарий Эрика Липперта) заставляет меня беспокоиться о моем использовании volatile:когда ключевое слово volatile должно использоваться в c# ?

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

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

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

после чтения 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();
        }
    }

5 ответов


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

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

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

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

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

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

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

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

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

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

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

во-первых, целые числа уже составляют всего 32 бита.

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

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

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

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


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

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


по словам Джо Албахари отлично сообщение о потоках и замках который из его столь же отличной книги C# 5.0 в двух словах, он говорит здесь даже когда используется ключевое слово volatile, оператор write, за которым следует оператор read, может быть переупорядочен.

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

таким образом, использование блокировки не только более "правильно", но и более понятно, предлагает простоту добавления новой функции, которая добавляет несколько операторов обновления в код атомарным способом - который ни Летучий, ни класс забора, как перед вызовом memorybarrier смогите сделать, очень быстро, и гораздо легке для поддержания С там гораздо меньше шансов на введение тонкой ошибки менее опытными разработчиками вниз по линии.


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

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

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

редактировать

вы рассматривали использование Interlocked.Приращение?


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