Здравствуйте, Sharov, Вы писали:
S>Мне казалось, что Join() и есть барьер, т.е. он обновит\синхронизирует переменные потока (звучит странно, да).
Тут проблема не в собственно в барьере, а в оптимизациях jit. Он выносит чтение поля из тела цикла.
Therefore, language compilers, the CPU, and the JIT compiler will not reorder memory operations (read or write) that appear in your program before the call to MemoryBarrier so that they occur after the call to MemoryBarrier when the program is executed. Similarly, memory accesses that appear in your program after the call will not be executed before the call.
(с). Т.е. явный вызов Thread.MemoryBarrier() не позволяет кэшировать isComplete в локальной переменной.
По факту же, получить стабильно зависающий пример на x86/x64 довольно сложно. Даже простой вызов (раскомментировать CallMe):
public static void Main(string[] args)
{
bool isComplete = false;
var t = new Thread(() =>
{
int i = 0;
while (!isComplete)
{
i += 0;
//CallMe(i);
}
});
t.Start();
Thread.Sleep(500);
isComplete = true;
t.Join();
Console.WriteLine("complete!");
}
private static void CallMe(int i)
{
i++;
}
— и мы получаем "рабочий" код, с непредсказуемым шансом на дедлок. Документировано только то, что такой код _может_ не работать, но никак не обратное.
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, vasmann, Вы писали:
V>>Ну если уж совсем быть точным, то зависит от настройки сборки, если вместе /checked собрана, то не вечно — упадет с OverflowException-ом. V>>Я закончил S>
S>Ок, добиваем.
S>Во-первых, коварность этой ошибки в том, что она не воспроизводится ни на отладочных сборках, ни в релизных с подключённым отладчиком (пока в настройках не снята галочка "Suppress JIT optimization on module load"), ни в отдельных environments (пруф с mono).
Никак не удалось воспроизвести: x86, release, optimize code, галочка "Suppress JIT optimization on module load" снята. Полет нормальный. ЧЯДНТ?
Здравствуйте, vasmann, Вы писали:
V>Замыкание, как следствие анонимный тип с полем isComplet не валатильным.
Ну и как следствие (вроде очевидное, но решил написать): есть вероятность, что значение закешируется и изменения переменной isComplete из основного потока никак не повлияет на дочерний, таким образом дочерний будет крутиться вечно.
Здравствуйте, Sinix, Вы писали:
S>Собственно вопрос.
Обожаю лучшие ответы на SO:
Of course, you cannot add it, because it's a local variable. And of course, since it is a local variable, it should not be needed at all, because locals are kept on stack and they are naturally always "fresh".
С какой это радости стэк вдруг стал чем-то особенным? Стэк ведь обычный кусок памяти со всеми вытекающими. В некоторых языках передают ссылки/указатели на стэк, как часть нормального процесса. Даже в шарпе можно это сделать.
Здравствуйте, vasmann, Вы писали:
V>Ну если уж совсем быть точным, то зависит от настройки сборки, если вместе /checked собрана, то не вечно — упадет с OverflowException-ом. V>Я закончил
Ок, добиваем.
Во-первых, коварность этой ошибки в том, что она не воспроизводится ни на отладочных сборках, ни в релизных с подключённым отладчиком (пока в настройках не снята галочка "Suppress JIT optimization on module load"), ни в отдельных environments (пруф с mono).
UPD, с подачи ув. Sharov: всё ещё забавней. Конкретно этот код отрабатывает нормально с x86 JIT. А вот вариант с SO — нет.
Мораль: интеграционные тесты таки стоит запускать и в отладочной, и в релизной версиях. И на тех рантаймах, где код будет работать.
Здравствуйте, fddima, Вы писали:
F> С какой это радости стэк вдруг стал чем-то особенным? Стэк ведь обычный кусок памяти со всеми вытекающими. В некоторых языках передают ссылки/указатели на стэк, как часть нормального процесса. Даже в шарпе можно это сделать.
Да-да-да, и не в преферанс, а в спортлото и далее по тексту. Вот это полезней будет.
Ну и про "в шарпе можно это сделать" — спасиб, напомнили про очередную тему для минутки хардкора. Ща всё будеттынц
Здравствуйте, Sinix, Вы писали:
F>> С какой это радости стэк вдруг стал чем-то особенным? Стэк ведь обычный кусок памяти со всеми вытекающими. В некоторых языках передают ссылки/указатели на стэк, как часть нормального процесса. Даже в шарпе можно это сделать. S>Да-да-да, и не в преферанс, а в спортлото и далее по тексту. Вот это полезней будет.
Сейчас немного лень разбираться, что конкретно они там в issue имеют ввиду. Понятно замыкания и всё такое, я не шибкий их фанат ещё с первых версий — простейший код легко превращается в ерунду (не помню реальный пример, но псевдопример такой):
void OnSomeEvent(int ev)
{
// (1) замыкание создаётся тут.if (ev == 0) return;
// (2) а должно по месту использования
// И не факт что нужно создавать один огромный объект для всех последующих бранчей, если здесь будут ветвления.
// Понятно, что на каждый вызов необходимо переиспользовать holder, но это нужно делать как оптимизацию,
// а не как пессимизацию и создавать holder всегда со всем чем можно.
// Таким незамысловатым образом в C++ с ручным bind - получается наилучший код. Позволю себе всем напомнить, что
// неважно как быстро аллоцируются объекты - ещё быстрее это не аллоцировать их вообще, если не нужны.
// (1) написан именно для этого, но сюрприз-сюрприз... в шарпе - альтернатива только разбить метод на два,
// что на мой взгляд с точки зрения ежедневной работы с кодом - ещё хуже чем "холостая" аллокация.
dispatcher.Queue(() => OnSomeEventInternal(ev));
}
Не проверял на последних компиляторах, но это я к тому — что понятно, что без них не жизнь, но основные кейсы — не требуют никаких volatile. И имхо, если код становится нетривиальным/нелинейным, да ещё и вокруг потоков — замыкания ни разу не друзья. Потом это гораздо тяжелее дебажить, а колстэки менее информативны (т.к. указывают не на уникальное имя метода, а на один "здоровый").
Но и там в коментах проскакивает то, что volatile нафиг не упал в текущих .NET, да и всепрощающий x86/amd64 тоже не способствует к "тру" коду... Не понимаю как вообще об этом всём можно думать в контексте .NET: рантайм творит не совсем то, что в спецификации. Имхо, всё ещё куча кода просто не сможет работать ни на слабой модели памяти, ни с более серьёзным оптимизатором/пессимизатором:
Старый добрый шаблон (1):
void OnMyEvent()
{
if (MyEvent != null) MyEvent();
}
Все мы помним, что правильно это делать так (2):
void OnMyEvent()
{
var handler = MyEvent;
if (handler != null) handler();
}
Забудем про оператор "?." т.к. он здесь не может ничего добавить.
Так вот, компилятор волен "пессимизировать" здесь, и развернуть к коду как в варианте (1). Ровно как и (1) компилятор волен привести к (2). При этом никаких средств управлять этим нет. Насколько я знаю ни компилятор ни JIT таким не занимаются, но им никто не мешает это делать в будущем. Какие замыкания и volatile, если гораздо более фундаментальные и простые вопросы никак не решены / не специфицированы?
PS: Вполне вероятно я что-то из нового упустил и этот вопрос специфицирован.
Здравствуйте, Sharov, Вы писали:
S>Никак не удалось воспроизвести: x86, release, optimize code, галочка "Suppress JIT optimization on module load" снята. Полет нормальный. ЧЯДНТ?
Всё так — конкретно этот пример под x86 отрабатывает нормально, только под x64. А вот если чуть-чуть поменять (вариант из SO)…
public static void Main(string[] args)
{
bool isComplete = false;
var t = new Thread(() =>
{
int i = 0;
while (!isComplete) i += 0;
});
t.Start();
Thread.Sleep(500);
isComplete = true;
t.Join();
Console.WriteLine("complete!");
}
простенькое… S>Народ, давайте тож участвуйте. Веселее будет.
S>Назад к делу S>
S> public static void Main(string[] args)
S> {
S> bool isComplete = false;
S> int i = 0;
S> var t = new Thread(() =>
S> {
S> while (!isComplete)
S> i++;
S> });
S> t.Start();
S> Thread.Sleep(500);
S> isComplete = true;
S> t.Join();
S> Console.WriteLine("complete! loops:" + i);
S> Console.ReadKey();
S> }
S>
Мне казалось, что Join() и есть барьер, т.е. он обновит\синхронизирует переменные потока (звучит странно, да). Т.е. я думал тут возможен косяк в логике, где вроде ожидаем результат сразу после "isComplete = true;", а на деле только поcле Join.