Может ли поток C # действительно кэшировать значение и игнорировать изменения этого значения в других потоках? - PullRequest
25 голосов
/ 19 января 2009

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

ОБНОВЛЕНИЕ: Мой вопрос не в том, «существует ли странность с изменчивой памятью» (я знаю, что она существует), а в моем вопросе «разве среда выполнения .NET так не абстрагируется, что вы ее никогда не увидите».

См. http://www.yoda.arachsys.com/csharp/threads/volatility.shtml и первый ответ на Является ли свойство строки самим потокобезопасным?

(Это на самом деле одна и та же статья, поскольку один ссылается на другой.) Один поток устанавливает логическое значение, а другой - циклы, постоянно читающие этот логический элемент - в этих статьях утверждается, что поток чтения может кэшировать старое значение и никогда не читать новое. значение, поэтому вам нужна блокировка (или используйте ключевое слово volatile). Они утверждают, что следующий код потенциально зациклится навсегда. Теперь я согласен, что это хорошая практика - блокировать ваши переменные, но я не могу поверить, что среда выполнения .NET действительно проигнорирует изменение значения памяти, как утверждается в статье. Я понимаю их разговор о энергозависимой памяти против энергонезависимой памяти, и я согласен, что у них есть действительная точка в неуправляемом коде, но я не могу поверить, что среда выполнения .NET не будет правильно абстрагироваться от этого так что следующий код делает то, что вы ожидаете. В статье даже признается, что код будет «почти наверняка» работать (хотя и не гарантировано), поэтому я звоню в BS по поводу претензии. Кто-нибудь может проверить, что это правда, следующий код не всегда будет работать? Может ли кто-нибудь получить хотя бы один случай (возможно, вы не всегда можете воспроизвести его), где это не удается?

class BackgroundTaskDemo
{
    private bool stopping = false;

    static void Main()
    {
        BackgroundTaskDemo demo = new BackgroundTaskDemo();
        new Thread(demo.DoWork).Start();
        Thread.Sleep(5000);
        demo.stopping = true;
    }

    static void DoWork()
    {
         while (!stopping)
         {
               // Do something here
         }
    }
}

Ответы [ 3 ]

49 голосов
/ 19 января 2009

Дело в том, что это может работать, но это не гарантировано для работы по спецификации. Люди обычно ищут код, который работает по правильным причинам, а не из-за случайной комбинации компилятора, среды выполнения и JIT, который может меняться в зависимости от версии платформы, физического ЦП, платформы и такие вещи, как x86 против x64.

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

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

Нет, у меня нет руки.


Обновление с повторяющимся контрпримером:

using System.Threading;
using System;
static class BackgroundTaskDemo
{
    // make this volatile to fix it
    private static bool stopping = false;

    static void Main()
    {
        new Thread(DoWork).Start();
        Thread.Sleep(5000);
        stopping = true;


        Console.WriteLine("Main exit");
        Console.ReadLine();
    }

    static void DoWork()
    {
        int i = 0;
        while (!stopping)
        {
            i++;
        }

        Console.WriteLine("DoWork exit " + i);
    }
}

Выход:

Main exit

но все еще работает, при полной загрузке процессора; обратите внимание, что stopping был установлен на true к этому моменту. ReadLine означает, что процесс не завершается. Похоже, что оптимизация зависит от размера кода внутри цикла (следовательно, i++). Это работает только в режиме «релиз», очевидно. Добавьте volatile и все работает нормально.

4 голосов
/ 19 января 2009

Этот пример включает собственный код x86 в качестве комментариев, чтобы продемонстрировать, что управляющая переменная (stopLooping) кэшируется.

Измените «stopLooping» на volatile, чтобы «исправить» его.

Он был собран с Visual Studio 2008 как сборка выпуска и запускается без отладки.

 using System;

 using System.Threading;

/* A simple console application which demonstrates the need for 
 the volatile keyword and shows the native x86 (JITed) code.*/

static class LoopForeverIfWeLoopOnce
{

    private static bool stopLooping = false;

    static void Main()
    {
        new Thread(Loop).Start();
        Thread.Sleep(1000);
        stopLooping = true;
        Console.Write("Main() is waiting for Enter to be pressed...");
        Console.ReadLine();
        Console.WriteLine("Main() is returning.");
    }

    static void Loop()
    {
        /*
         * Stack frame setup (Native x86 code):
         *  00000000  push        ebp  
         *  00000001  mov         ebp,esp 
         *  00000003  push        edi  
         *  00000004  push        esi  
         */

        int i = 0;
        /*
         * Initialize 'i' to zero ('i' is in register edi)
         *  00000005  xor         edi,edi 
         */

        while (!stopLooping)
            /*
             * Load 'stopLooping' into eax, test and skip loop if != 0
             *  00000007  movzx       eax,byte ptr ds:[001E2FE0h] 
             *  0000000e  test        eax,eax 
             *  00000010  jne         00000017 
             */
        {
            i++;
            /*
             * Increment 'i'
             *  00000012  inc         edi  
             */

            /*
             * Test the cached value of 'stopped' still in
             * register eax and do it again if it's still
             * zero (false), which it is if we get here:
             *  00000013  test        eax,eax 
             *  00000015  je          00000012 
             */
        }

        Console.WriteLine("i={0}", i);
    }
}
2 голосов
/ 19 января 2009

FWIW:

  • Я видел эту оптимизацию компилятора из компилятора MS C ++ (неуправляемый код).
  • Я не знаю, происходит ли это в C #
  • Этого не произойдет во время отладки (оптимизация компилятора автоматически отключается при отладке)
  • Даже если такая оптимизация не произойдет сейчас, вы держите пари, что они никогда не представят эту оптимизацию в будущих версиях JIT-компилятора.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...