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

Может ли поток С# кэшировать значение и игнорировать изменения этого значения для других потоков?

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

UPDATE: Мой вопрос не "существует ли странность с энергозависимой памятью" (я знаю, что это так), мой вопрос: "Это не абстрактное время выполнения .NET, поэтому вы никогда его не увидите".

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

(Они действительно одна и та же статья, так как одна ссылается на другую.) Один поток устанавливает bool, а другие петли потоков навсегда читают этот bool - эти статьи утверждают, что поток чтения может кэшировать старое значение и никогда не читать новое значение, поэтому вам нужна блокировка (или используйте ключевое слово 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
         }
    }
}
4b9b3361

Ответ 1

Дело в том, что это может сработать, но не гарантировано работать по спецификации. То, что обычно происходит людьми, - это код, который работает по правильным причинам, вместо того, чтобы работать с помощью сочетания компилятора, среды выполнения и JIT, которые могут меняться между версиями фреймворка, физическим процессором, платформой и такими вещами, как x86 vs 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, и все работает нормально.

Ответ 2

Этот пример включает собственный код 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 still
             * zero (false), which it is if we get here:
             *  00000013  test        eax,eax 
             *  00000015  je          00000012 
             */
        }

        Console.WriteLine("i={0}", i);
    }
}

Ответ 3

FWIW:

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