Подтвердить что ты не робот

С# - Использование ключевого слова volatile vs lock

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

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

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

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

Прочитав 2 первых ответа. Я хотел бы настаивать на том, что, как написано код, не важно, если из-за concurrency мы пропустим приращение (хотим увеличивать с 2 потоков, но результат увеличивается только на 1 из-за concurrency), если хотя бы переменная "_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();
        }
    }
4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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

ИЗМЕНИТЬ

Считаете ли вы использование Interlocked.Increment?

Ответ 5

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