Может ли поток 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-компилятора.