Почему эта программа не заходит в бесконечный цикл при отсутствии изменчивости логической переменной условия? - PullRequest
4 голосов
/ 03 января 2012

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

Два вопроса:

  1. Что я должен изменить в приведенном ниже листинге кода - чтобы он абсолютно требовал использования volatile?

  2. Достаточно ли умен компилятор C # для обработки переменной как изменчивой - если он видит, что к переменной обращаются из другого потока?

Вышеприведенное вызвало у меня больше вопросов:)

а.Является ли волатильность лишь намеком?

б.Когда я должен объявить переменную как volatile в контексте многопоточности?

c.Должны ли все переменные-члены быть объявлены как volatile для класса, безопасного для потоков?Это перебор?

Перечень кодов (в центре внимания волатильность, а не безопасность потоков):

class Program
{
    static void Main(string[] args)
    {
        VolatileDemo demo = new VolatileDemo();
        demo.Start();

        Console.WriteLine("Completed");
        Console.Read();
    }
}

    public class VolatileDemo
    {
        public VolatileDemo()
        {
        }

        public void Start()
        {
            var thread = new Thread(() =>
            {
                Thread.Sleep(5000);
                stop = true;
            });

            thread.Start();

            while (stop == false)
                Console.WriteLine("Waiting For Stop Event");
        }

        private bool stop = false;
    }

Спасибо.

Ответы [ 5 ]

4 голосов
/ 03 января 2012

Во-первых, Джо Даффи говорит: «изменчиво - это зло» - этого достаточно для меня.

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

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

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

Этот код требует volatile и никогда не завершится без Volatile.Read:

static void Run()
{
    bool stop = false;

    Task.Factory.StartNew( () => { Thread.Sleep( 1000 ); stop = true; } );

    while ( !stop ) ;
}
3 голосов
/ 03 января 2012

Я не эксперт по параллелизму в C #, но AFAIK ваши ожидания неверны. Изменение энергонезависимой переменной из другого потока не означает, что изменение никогда не станет видимым для других потоков. Только в том, что нет гарантии, когда (и если) это произойдет . В вашем случае это произошло (сколько раз вы запускали программу, кстати?), Возможно, из-за того, что завершающий поток сбрасывал свои изменения в соответствии с комментарием @ Russell. Но в реальной жизни - с более сложным потоком программ, большим количеством переменных, большим количеством потоков - обновление может произойти позже, чем через 5 секунд, или - может быть, один раз в тысячу случаев - может вообще не бывает.

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

2 голосов
/ 03 января 2012

Попробуйте переписать это так:

    public void Start()
    {
        var thread = new Thread(() =>
        {
            Thread.Sleep(5000);
            stop = true;
        });

        thread.Start();

        bool unused = false;
        while (stop == false)
            unused = !unused; // fake work to prevent optimization
    }

И убедитесь, что вы работаете в режиме Release, а не в режиме Debug. В режиме Release применяются оптимизации, которые фактически приводят к сбою кода при отсутствии volatile.

Редактировать : Немного о volatile:

Все мы знаем, что в жизненный цикл программы вовлечены две различные сущности, которые могут применять оптимизации в форме кэширования переменных и / или переупорядочения команд: компилятор и ЦП.

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

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

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

1 голос
/ 03 января 2012

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

Вызов функции к Console.WriteLine является полным ограничением памяти, и его семантика возможного выполнения чего-либо (кроме оптимизаций компилятора) потребует, чтобы stop не кэшировался.

Однако, если вы удалите вызов Console.WriteLine, я обнаружу, что функция все еще останавливается.

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

Этот код все еще останавливается (по крайней мере, для меня, я использую Mono):

public void Start()
{
    stop = false;

    var thread = new Thread(() =>
    {
        while(true)
        {
            Thread.Sleep(50);
            stop = !stop;
        }
    });

    thread.Start();

    while ( !(stop ^ stop) );
}

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

Эта оптимизация выглядит чувствительной к модели памяти, которая зависит от платформы, что означает, что это будет сделано в компиляторе JIT; у которого не было бы времени (или интеллекта) для / просмотра / использования переменной в другом потоке и предотвращения кеширования по этой причине.

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

1 голос
/ 03 января 2012

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

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

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

...