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

Полезны ли изменчивые переменные? Если да, то когда?

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

[начать редактирование] Возможно, это не так очевидно (итальянский юмор?!), но заголовок просто довольно провокационный: конечно, должна быть причина, если volatile был включен в С#, я просто не могу понять точный. [end edit]

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

  • lock, поскольку это предотвратит переупорядочение команд.
  • volatile, потому что заставит CPU всегда читать значение из памяти (тогда разные ЦП/ядра не будут кэшировать его, и они не будут видеть старые значения).
  • Заблокированные операции (Increment/Decrement и CompareExchange), потому что они будут выполнять изменение + присваивание в одном атоме ( fast быстрее, чем, например, волатильная + блокировка) работа.

То, что я не понимаю (ссылка на спецификации С# будет оценена):

  • Как блокировка предотвратит проблему с кешем? Является ли это неявным барьером памяти в критическом разделе?
  • Летучие переменные не могут быть локальными (я читал что-то от Эрика Липперта об этом, но я не могу найти этот пост сейчас, и я не помню его комментариев и, если честно, я даже не понял его Что ж). Это заставляет меня думать, что они не реализованы с Interlocked.CompareExchange() и друзьями, в чем они отличаются?

Какой модификатор volatile будет делать, например, в этом коде?

volatile int _volatileField = 0;
int _normalField = 0;

void test()
{
    Interlocked.Increment(ref _normalField);
    ++_volatileField;
}

[начать редактирование] Предыдущий пример включает в себя атомарное чтение + запись, пусть оно изменится на _volatileField = 1;, здесь я не говорю об атомных операциях. [end edit]

Более того, какой компилятор (помимо предупреждений) будет делать здесь:

Interlocked.Increment(ref _volatileField);

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

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

[начать редактирование] Я нашел предыдущее предложение действительно непонятным (извините за мой английский). Я имею в виду: если производительность (volatile по сравнению с CompareExchange, где сравнение применимо) лучше (да, мы можем измерить, а в некоторых случаях разница измерима и видима), то почему мы не можем использовать их для локальных переменные? Я думаю о параллельных циклах, которые манипулируют множеством данных (где и накладные расходы, и барьеры могут сильно повредить производительность). [End edit]

4b9b3361

Ответ 1

Этот вопрос очень запутан. Позвольте мне попытаться сломать его.

Полезны ли изменчивые переменные?

Да. Команда С# не добавила бы бесполезную функцию.

Если да, то когда?

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

Как редакционная статья в сторону, я отмечаю, что для обычных программистов С# для нормальной работы в любой из этих ситуаций должно быть редко. Во-первых, характеристики производительности, о которых мы говорим, составляют порядка десятков наносекунд; большинство приложений LOB имеют требования к производительности, измеренные в секундах или минутах, а не в наносекундах. Во-вторых, большинство приложений LOB С# могут выполнять свою работу только с небольшим количеством потоков. В-третьих, разделяемая память - плохая идея и причина многих ошибок; Приложения LOB, которые используют рабочие потоки, не должны напрямую использовать потоки, а скорее используют параллельную библиотеку задач для безопасного указания рабочих потоков выполнять вычисления и затем возвращать результаты. Рассмотрите возможность использования нового ключевого слова await в С# 5.0, чтобы облегчить асинхронность на основе задач, а не напрямую использовать потоки.

Любое использование волатильности в приложении LOB является большим красным флагом и должно быть тщательно проверено экспертами и идеально устранено в пользу более высокой и менее опасной практики.

Блокировка

предотвратит переупорядочение команд.

Блокировка описывается спецификацией С# как особая точка в коде, так что определенные специальные побочные эффекты гарантированно упорядочиваются определенным образом относительно ввода и выхода из замка.

volatile, потому что заставит CPU всегда считывать значение из памяти (тогда разные ЦП/ядра не будут кэшировать его, и они не будут видеть старые значения).

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

Взаимоблокированные операции выполняют изменение + присвоение в одной атомной (быстрой) операции.

Мне непонятно, почему вы поставили "скорую" в скобках после "атомного"; "fast" не является синонимом "атомарного".

Как блокировка предотвратит проблему с кешем?

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

Является ли это неявным барьером памяти в критическом разделе?

На практике да, замок вводит полный забор.

Летучие переменные не могут быть локальными

Правильно. Если вы обращаетесь к локальному из двух потоков, то локальный должен быть специальным локальным: это может быть закрытая внешняя переменная делегата или в блоке асинхронизации или в блоке итератора. Во всех случаях локальный фактически реализуется как поле. Если вы хотите, чтобы такая вещь была энергозависимой, тогда не используйте высокоуровневые функции, такие как анонимные методы, блоки асинхронных блоков или блоки итераторов! Это смешивание самого высокого уровня и самого низкого уровня кодирования С#, и это очень странная вещь. Напишите свой собственный класс закрытия и сделайте поля неустойчивыми, как вам удобно.

Я прочитал кое-что от Эрика Липперта об этом, но я не могу найти этот пост сейчас, и я не помню его ответа.

Ну, я тоже этого не помню, поэтому я набрал "Eric Lippert", почему локальная переменная не может быть изменчивой "в поисковой системе. Это заставило меня ответить на этот вопрос:

Почему локальная переменная не может быть изменчивой на С#?

Возможно, это то, о чем вы думаете.

Это заставляет меня думать, что они не реализованы с помощью Interlocked.CompareExchange() и друзей.

С# реализует изменчивые поля как изменчивые поля. Неустойчивые поля являются фундаментальной концепцией в CLR; как CLR реализует их, является детальностью реализации CLR.

в чем они отличаются?

Я не понимаю вопроса.

Какой изменчивый модификатор будет делать, например, в этом коде?

++_volatileField;

Он ничего не помогает, поэтому не делайте этого. Волатильность и атомарность - это совершенно разные вещи. Выполнение нормального приращения в энергозависимом поле не увеличивает прирост в атомном приращении.

Более того, какой компилятор (помимо предупреждений) будет делать здесь:

Компилятор С# действительно должен подавить это предупреждение, если вызываемый метод вводит забор, как это делает. Мне так и не удалось получить это в компиляторе. Надеюсь, команда когда-нибудь будет.

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

Как это возможно для нелетучих полей?

Эта деталь реализации CLR.

Они также подразумевают барьеры?

Да, блокированные операции создают барьеры. Опять же, это подробная информация о реализации.

Разве это не сильно ухудшает производительность (по сравнению с изменчивым)?

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

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

Если волатильность не подразумевает барьеры, но другие делают то, почему мы не можем использовать их как локальные переменные?

Я даже не могу начать понимать этот вопрос.

Ответ 2

Изменчивые переменные могут быть полезны при использовании кода следующим образом:

while (myVolatileFlag)
    ...

Если myVolatileFlag объявлен как volatile bool, он не позволит компилятору кешировать его значение и предположить, что он не изменится во время цикла. (Однако на самом деле довольно сложно написать некоторый код, который на самом деле демонстрирует разницу, которую делает применение volatile.)

От http://msdn.microsoft.com/en-us/LIBRARY/x13ttww7%28v=vs.80%29.aspx

Ключевое слово volatile указывает, что поле может быть изменено несколько одновременных выполнения потоков. Поля, объявленные volatile не подпадают под оптимизацию компилятора, которые предполагают доступ одним потоком. Это гарантирует, что самое современное значение постоянно присутствовать в поле.

Вот пример программы, демонстрирующий проблему:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    internal class Program
    {
        private void run()
        {
            Task.Factory.StartNew(resetFlagAfter1s);
            int x = 0;

            while (flag)
                ++x;

            Console.WriteLine("Done");
        }

        private void resetFlagAfter1s()
        {
            Thread.Sleep(1000);
            flag = false;
        }

        private volatile bool flag = true;

        private static void Main()
        {
            new Program().run();
        }
    }
}

Запустите сборку "Release" вышеуказанной программы, и она прекратится через одну секунду. Удалите модификатор volatile с volatile bool flag, и он никогда не завершится.

Летучие локали

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

Однако, с более поздними версиями С# с Lambdas и т.д., все не так ясно. См. ответ Эрика Липперта в этой теме.

Ответ 3

Как lock предотвратит проблему с кешем? Является ли это неявным барьером памяти? в критическом разделе?

Да, lock также действует как полный забор (он имеет как семантику получения и освобождения). Эта страница в центре Windows dev объясняет, как это работает:

Процессоры могут поддерживать инструкции для барьеров памяти с приобретением, релиз и семантика. Эти семантики описывают порядок в какие результаты операции становятся доступными. С приобретать семантики, результаты операции доступны до результаты любой операции, которая появляется после нее в коде. С выпуском семантики, результаты операции доступны после результаты любой операции, которая появляется перед ним в коде. Забор семантика объединяет семантику получения и выпуска. Результаты операция с семантикой забора доступна до который появляется после него в коде и после который появляется перед ним.


Летучие переменные не могут быть локальными (я читал что-то от Эрика Липперта об этом, но я не могу найти этот пост сейчас, и я не помню его ответ). Это заставляет меня думать, что они не реализованы с помощью Interlocked.CompareExchange() и друзей, в чем они отличаются?

Какой изменчивый модификатор будет делать, например, в этом коде?

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

volatile на примере кода не будет многого. Он будет уверен, что когда последовательность чтения/изменения/сохранения для приращения _volatileField начинается, чтение будет фактически переходить в память вместо того, чтобы быть возможно удовлетворенным от кеша процессора, но это не поможет вообще с условиями гонки, если есть другие потоки, которые одновременно записываются в поле.