Цей код завис у режимі випуску, але добре працює у режимі налагодження


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
у чому саме різниця в поведінці?
Монг Чжу

4
Якби це Java, я вважаю, що компілятор не бачить оновлень змінної 'compile'. Додавання "летючого" до змінної декларації дозволило б це виправити (і зробити його статичним полем).
Себастьян


4
Примітка: саме тому багатопотокове розроблення має такі речі, як мутекси та атомні операції. Коли ви почнете переходити до багатопотокових, вам слід розглянути чудовий набір додаткових проблем із пам'яттю, які раніше не були очевидними. Інструменти синхронізації потоків, такі як mutex, вирішили б це.
Cort Ammon

5
@DavidSchwartz: Безумовно, це дозволено . І це дозволено використовувати компілятор, середовище програмного продукту та CPU , щоб результат цього коду буде відрізнятися від того, що ви очікуєте. Зокрема, C # заборонено робити доступ до bool неатомічним , але дозволено переміщувати енергонезалежні читання назад у часі. Парні, навпаки, не мають такого обмеження на атомність; подвійне читання та написання на двох різних потоках без синхронізації дозволено розривати.
Ерік Ліпперт

Відповіді:


149

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

Різниця в режимі налагодження виникає, ймовірно, тому, що в режимі налагодження виключається багато оптимізацій, тож ви можете налагодити код, який ви бачите на екрані. Тому while (!isComplete)не оптимізовано, тому ви можете встановити там точку розриву, і тому isCompleteне агресивно кешується в регістрі чи стеку при запуску методу і зчитується з об'єкта на купі при кожній ітерації циклу.

До речі. Це лише мої здогадки про це. Я навіть не намагався її скласти.

До речі. Це не здається помилкою; це більше схоже на дуже незрозумілий побічний ефект. Крім того, якщо я маю рацію щодо цього, то це може бути дефіцит мови - C # повинен дозволяти розміщувати "мінливе" ключове слово на локальних змінних, які фіксуються та просуваються до полів учасників у закритих місцях.

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


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

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

3
Зрозумів. Я відмовляюся від спроб зрозуміти багатопотокові оптимізації ... це божевільно, наскільки це складно.
Між

10
@Pikoh: Знову ж таки, думайте як оптимізатор. У вас є змінна, яка збільшується, але ніколи не читається. Змінна, яка ніколи не читається, може бути видалена повністю.
Ерік Ліпперт

4
@EricLippert тепер мій розум здійснив клацання. Ця тема була дуже інформативною, дуже дякую, справді.
Піко

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
дивіться мою відповідь, я майже впевнений, що все стосується ключового слова, яке не може бути додано до локальної змінної (яка фактично пізніше буде переведена в поле члена під час закриття) ..
quetzalcoatl
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.