Этот код зависает в режиме выпуска, но отлично работает в режиме отладки


110

Я наткнулся на это и хочу узнать причину такого поведения в режиме отладки и выпуска.

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!");
}

25
в чем именно разница в поведении?
— Mong Zhu

4
Если бы это была java, я бы предположил, что компилятор не видит обновления переменной compile. Добавление «volatile» к объявлению переменной исправит это (и сделает его статическим полем).
— Себастьян


4
Примечание: вот почему многопоточная разработка имеет такие вещи, как мьютексы и атомарные операции. Когда вы начинаете работать с многопоточностью, вам нужно учитывать ряд замечательных проблем с памятью, которые раньше не были очевидны. Такие инструменты синхронизации потоков, как мьютексы, могли бы решить эту проблему.
— Cort Ammon

5
@DavidSchwartz: Конечно, это разрешено . И компилятору, среде выполнения и процессору разрешено делать результат этого кода отличным от ожидаемого. В частности, C # не разрешено делать доступ bool неатомарным , но разрешено перемещать энергонезависимые чтения назад во времени. Двойники, напротив, не имеют такого ограничения атомарности; двойное чтение и запись в двух разных потоках без синхронизации может быть разорвано.
— Эрик Липперт,

Ответы:


149

Я предполагаю, что оптимизатор введен в заблуждение из-за отсутствия ключевого слова 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() метода и никогда не обновлять его, пока метод не завершится. Это потому, что НЕТ ГАРАНТИИ, что поток и метод НЕ завершатся до того, как будет выполнено "= true", поэтому, если нет гарантии, то почему бы не кэшировать его и не повысить производительность, прочитав объект кучи один раз вместо того, чтобы читать его каждый раз итерация.

Именно поэтому ключевое слово volatile существует.

Чтобы этот вспомогательный класс был правильным, немного лучше 1) в многопоточных средах он должен иметь:

    public volatile bool isComplete = false;

но, конечно, поскольку это автоматически сгенерированный код, вы не можете его добавить. Лучшим подходом было бы добавить некоторые lock()s вокруг операций чтения и записи isCompletedили использовать какие-либо другие готовые к использованию утилиты синхронизации или потоковой передачи / задания задач вместо того, чтобы пытаться сделать это с нуля (что не будет голым железом, так как это C # на CLR с GC, JIT и (..)).

Разница в режиме отладки возникает, вероятно, из-за того, что в режиме отладки исключается множество оптимизаций, поэтому вы можете отлаживать код, который видите на экране. Поэтому while (!isComplete)не оптимизирован, поэтому вы можете установить там точку останова, и поэтомуisComplete , не кэшируется агрессивно в регистре или стеке при запуске метода и считывается из объекта в куче на каждой итерации цикла.

КСТАТИ. Это всего лишь мои догадки. Я даже не пытался его скомпилировать.

КСТАТИ. Это не похоже на ошибку; это больше похоже на очень неясный побочный эффект. Кроме того, если я прав, тогда это может быть недостаток языка - C # должен позволять размещать ключевое слово volatile в локальных переменных, которые захватываются и продвигаются в поля-члены в замыканиях.

1) смотри ниже за комментариями Эрика Липпертом о volatileи / или в этом очень интересную статью с указанием уровня сложности , участвующих в обеспечении того , чтобы код , опираясь на volatileэто безопасно ..uh, хорошо ..uh, скажем OK.


2
@EricLippert: эй, большое спасибо, что так быстро это подтвердили! Как вы думаете, есть ли шанс, что в какой-то будущей версии мы сможем получить volatileвозможность захвата для закрытия локальных переменных? Я предполагаю , что это может быть немного трудно обрабатывать компилятором ..
— Кетцалькоатль

7
@quetzalcoatl: Я бы не стал рассчитывать на добавление этой функции в ближайшее время. Это тот вид программирования, который вы хотели бы отбить , а не облегчить . Кроме того, создание нестабильности не обязательно решает все проблемы. Вот пример, когда все нестабильно, а программа по-прежнему неверна; ты можешь найти ошибку? blog.coverity.com/2014/03/26/reordering-optimizations
— Эрик Липперт,

3
Понял. Я бросаю попытки понять оптимизацию многопоточности ... безумие, насколько это сложно.
— Между

10
@Pikoh: Опять же, думайте как оптимизатор. У вас есть переменная, которая увеличивается, но никогда не читается. Переменная, которая никогда не читается, может быть полностью удалена.
— Эрик Липперт

4
@EricLippert, теперь мой разум щелкнул. Эта ветка была очень информативной, спасибо вам большое.
— Pikoh

82

Ответ Кетцалькоатля правильно. Чтобы пролить на это больше света:

Компилятору C # и джиттеру CLR разрешено выполнять множество оптимизаций, предполагающих, что текущий поток является единственным запущенным потоком. Если эти оптимизации делают программу некорректной в мире, где текущий поток - не единственный запущенный поток , это ваша проблема . От вас требуется писать многопоточные программы, которые сообщают компилятору и джиттеру, какие сумасшедшие многопоточные вещи вы делаете.

В этом конкретном случае джиттер разрешен - но не обязателен - для наблюдения за тем, что переменная не изменяется телом цикла, и, следовательно, для заключения, что - поскольку по предположению это единственный запущенный поток - переменная никогда не изменится. Если он никогда не меняется, тогда переменную нужно проверить на истинность один раз. , а не каждый раз в цикле. Так и происходит.

Как это решить? Не пишите многопоточные программы . Наладить многопоточность невероятно сложно даже экспертам. Если нужно, используйте механизмы самого высокого уровня для достижения своей цели . Решение здесь - не делать переменную изменчивой. Решение здесь - написать отменяемую задачу и использовать механизм отмены параллельной библиотеки задач . Пусть TPL позаботится о правильной логике потоковой передачи и о правильной отправке отмены по потокам.


1
Комментарии не подлежат расширенному обсуждению; этот разговор был перемещен в чат .
— Призрак Мадары

14

Я подключился к запущенному процессу и обнаружил (если я не ошибался, я не очень практиковался в этом), что Threadметод переведен на это:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(который содержит значение isComplete) загружается в первый раз и никогда не обновляется.


8

Не совсем ответ, но чтобы пролить свет на проблему:

Проблема , кажется, когда iобъявляется внутри тела лямбды и это только прочитать в выражении присваивания. В остальном код хорошо работает в режиме выпуска:

  1. i объявлен вне тела лямбды:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i не читается в выражении присваивания:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i также читается где-то еще:

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode

Моя ставка - это какой-то компилятор или JIT-оптимизация, что iвсе портит. Кто-нибудь умнее меня, вероятно, сможет пролить больше света на эту проблему.

Тем не менее, я бы не стал особо беспокоиться об этом, потому что не вижу, где подобный код действительно может служить какой-либо цели.


1
см мой ответ, я уверен , что это все о «летучий» ключевое слово thatcannot быть добавлены к локальной переменной (которая на самом деле получает позже продвинут на поле члена в закрытии) ..
— Кетцалькоатль
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.