Ответ 1
Я предполагаю, что оптимизатор одурачен отсутствием ключевого слова "volatile" в переменной isComplete
.
Конечно, вы не можете добавить его, потому что это локальная переменная. И, конечно, поскольку это локальная переменная, она не нужна вообще, потому что локали хранятся в стек, и они, естественно, всегда "свежие".
Однако, после компиляции он больше не является локальной переменной. Поскольку он обращается к анонимному делегату, код разбивается и преобразуется в класс-помощник и поле участника, что-то вроде:
public static void Main(string[] args)
{
TheHelper hlp = new TheHelper();
var t = new Thread(hlp.Body);
t.Start();
Thread.Sleep(500);
hlp.isComplete = true;
t.Join();
Console.WriteLine("complete!");
}
private class TheHelper
{
public bool isComplete = false;
public void Body()
{
int i = 0;
while (!isComplete) i += 0;
}
}
Теперь я могу представить, что JIT-компилятор/оптимизатор в многопоточной среде при обработке класса TheHelper
может фактически кэшировать значение false
в каком-либо регистровом или стеке в начале метод Body()
и никогда не обновлять его до тех пор, пока метод не завершится. Это потому, что нет НИКАКОЙ ГАРАНТИИ, что метод thread & НЕ завершится до того, как будет выполнен "= true", поэтому, если нет гарантии, то почему бы не кэшировать его и не получить повышение производительности чтения объекта кучи один раз вместо его чтения на каждой итерации.
Именно поэтому существует ключевое слово volatile
.
Чтобы этот вспомогательный класс был правильный немного лучше 1) в многопоточных средах, он должен иметь:
public volatile bool isComplete = false;
но, конечно, так как это автогенерированный код, вы не можете его добавить. Лучшим подходом было бы добавить некоторые lock()
вокруг чтения и записи на isCompleted
или использовать некоторые другие готовые к использованию средства синхронизации или потоки/задания, вместо того, чтобы пытаться сделать это bare-metal (что он не будет bare-metal, так как он С# на CLR с GC, JIT и (..)).
Разница в режиме отладки происходит, вероятно, потому, что в режиме отладки многие оптимизации исключены, поэтому вы можете отлаживать код, который вы видите на экране. Поэтому while (!isComplete)
не оптимизирован, поэтому вы можете установить там точку останова, и поэтому isComplete
не агрессивно кэшируется в регистре или стеке при запуске метода и считывается с объекта в куче на каждой итерации цикла.
BTW. Это только мои догадки об этом. Я даже не пытался его скомпилировать.
BTW. Кажется, это не ошибка; это больше похоже на очень неясный побочный эффект. Кроме того, если я прав по этому поводу, то это может быть недостаток языка - С# должен позволить помещать ключевое слово "volatile" на локальные переменные, которые захватываются и продвигаются к полям-членам в закрытии.
1) см. ниже комментарии Эрика Липперта о volatile
и/или этой очень интересной статье, показывающей уровни сложности, связанные с обеспечением того, чтобы код, основанный на volatile
, был безопасно..uh, good..uh, скажем, ОК.