Может ли поток С# кэшировать значение и игнорировать изменения этого значения для других потоков?
Этот вопрос НЕ о гоночных условиях, атомарности или о том, почему вы должны использовать блокировки в своем коде. Я уже знаю о них.
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
}
}
}
Ответы
Ответ 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.