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

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

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!");
}
c#
108 голосов | спросил Susinder Rajendran 13 Jpm1000000pmFri, 13 Jan 2017 13:27:24 +030017 2017, 13:27:24

4 ответа


0

Я полагаю, что оптимизатор одурачен отсутствием ключевого слова 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, или использовать некоторые другие готовые утилиты для синхронизации или потоков /задач вместо того, чтобы пытаться сделать это голым металлом (который не будет голым металлом) , так как это C # на CLR с GC, JIT и (..)).

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

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

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

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

ответил quetzalcoatl 13 Jpm1000000pmFri, 13 Jan 2017 14:56:24 +030017 2017, 14:56:24
0

ответ quetzalcoatl правильный. Чтобы пролить больше света на это:

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

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

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

ответил Eric Lippert 13 Jpm1000000pmFri, 13 Jan 2017 15:12:11 +030017 2017, 15:12:11
0

Я присоединился к работающему процессу и обнаружил (если я не делал ошибок, я не очень практиковался с этим), что 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) загружается впервые и никогда не обновляется.

ответил Matteo Umili 13 Jpm1000000pmFri, 13 Jan 2017 14:30:00 +030017 2017, 14:30:00
0

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

Кажется, проблема в том, что 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. Кто-нибудь умнее меня, вероятно, сможет пролить больше света на этот вопрос.

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

ответил InBetween 13 Jpm1000000pmFri, 13 Jan 2017 13:47:07 +030017 2017, 13:47:07

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132