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

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

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

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

(Это на самом деле одна и та же статья, так как один ссылается на другой.) Один поток устанавливает 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
         }
    }
}

3 ответа

Решение

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

Этот пример включает в себя собственный код 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);
    }
}

FWIW:

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