Що саме відбувається, коли потік очікує завдання всередині циклу?


10

Після деякого часу, маючи справу з асинхронною / очікуваною схемою C #, я раптом зрозумів, що я не знаю, як пояснити, що відбувається в наступному коді:

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()передбачається повернути очікуване, Taskщо може або не спричинить перемикання потоку при виконанні продовження.

Я б не плутався, якби очікування не було в циклі. Я, природно, очікував, що решта методу (тобто продовження) потенційно може виконуватись на іншій нитці, що добре.

Однак усередині циклу поняття "решта методу" для мене стає трохи туманним.

Що станеться з "рештою циклу", якщо потік увімкнено продовження проти, якщо він не перемикається? На якій нитці виконується наступна ітерація циклу?

Мої спостереження показують (не остаточно підтверджено), що кожна ітерація починається на одній нитці (початковій), а продовження виконується на іншій. Це може бути насправді? Якщо так, то чи є це ступінь несподіваного паралелізму, який потрібно враховувати для безпеки потоку методу GetWorkAsync?

ОНОВЛЕННЯ: Моє запитання не є дублікатом, як пропонують деякі. while (!_quit) { ... }Кодовий просто спрощення мого фактичного коду. Насправді мій потік - це довговічний цикл, який обробляє свою вхідну чергу робочих елементів регулярно (кожні 5 секунд за замовчуванням). Фактична перевірка стану виходу - це не проста перевірка поля, як запропоновано зразковим кодом, а скоріше перевірка обробки події.



1
Див. Також Як прибутковість та очікування впровадження потоку управління в .NET? для отримання чудової інформації про те, як це все з'єднано разом.
Джон Ву

@John Wu: Я ще не бачив цієї нитки SO. Там багато цікавих інформаційних самородок. Дякую!
травня 1717

Відповіді:


6

Насправді це можна перевірити на « Спробуйте Рослін» . Ваш метод очікування буде переписаний void IAsyncStateMachine.MoveNext()на створений клас асинхронізації.

Що ви побачите, це щось подібне:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

По суті, не має значення, на якій нитці ви перебуваєте; державна машина може відновитись належним чином, замінивши цикл на еквівалентну структуру if / goto.

Сказавши це, методи асинхронізації не обов'язково виконуються на іншій нитці. Дивіться пояснення Еріка Ліпперта "Це не магія", щоб пояснити, як можна працювати async/awaitлише над однією ниткою.


2
Здається, я недооцінюю ступінь перезапису, який робить компілятор на моєму коді асинхронізації. По суті, після переписування немає "петлі"! Це була для мене відсутність. Приємно і дякую за посилання "Спробуйте Рослін"!
1717 року

GOTO - оригінальна конструкція циклу. Не забуваймо цього.

2

По-перше, Серві написав якийсь код у відповідь на подібне запитання, на якому ґрунтується ця відповідь:

/programming/22049339/how-to-create-a-cancellable-task-loop

Відповідь Серві включає аналогічний ContinueWith()цикл із використанням конструкцій TPL без явного використання ключових слів asyncта awaitключових слів; тож, щоб відповісти на ваше запитання, подумайте, як може виглядати ваш код, коли ваш цикл розкручується за допомогоюContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

Це займе певний час, щоб обернути голову, але підсумовуючи:

  • continuationявляє собою закриття для "поточної ітерації"
  • previousпредставляє стан, Taskщо містить стан "попередньої ітерації" (тобто він знає, коли "ітерація" закінчена і використовується для запуску наступної.)
  • Якщо припустити, що GetWorkAsync()повертає a Task, це означає ContinueWith(_ => GetWorkAsync()), що повернеться, Task<Task>отже, заклик Unwrap()отримати "внутрішнє завдання" (тобто фактичний результат GetWorkAsync()).

Тому:

  1. Спочатку немає попередньої ітерації, тому просто присвоюється значення Task.FromResult(_quit) - її стан починається як Task.Completed == true.
  2. continuationЗапускається в перший раз , використовуючиprevious.ContinueWith(continuation)
  3. continuationоновлення про закриття, previousщоб відобразити стан завершення_ => GetWorkAsync()
  4. Коли _ => GetWorkAsync()це завершено, воно "продовжується з" _previous.ContinueWith(continuation)- тобто continuationзнову викликає лямбда
    • Очевидно, що в цей момент previousбуло оновлено стан, _ => GetWorkAsync()тому continuationлямбда викликається при GetWorkAsync()поверненні.

continuationЛямбда завжди перевіряє стан _quitтак, якщо _quit == falseто немає більше продовжень, і TaskCompletionSourceотримує значення вартості _quit, і все завершується.

Що стосується ваших спостережень щодо продовження, яке виконується в іншій нитці, це не те, що async/ awaitключове слово зробило б для вас, відповідно до цього блогу "Завдання (все ще) не є потоками, а асинхронізація не паралельна" . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

Я б припустив, що дійсно варто детальніше ознайомитись із вашим GetWorkAsync()методом щодо безпеки різьби та ниток. Якщо ваша діагностика виявляє, що вона виконується на іншому потоці внаслідок повторного коду асинхронізації / очікування, то щось із цього методу або пов'язане з цим методом повинно викликати створення нового потоку в іншому місці. (Якщо це несподівано, можливо, є .ConfigureAwaitдесь?)


2
Я показав код (дуже) спрощений. Всередині GetWorkAsync () є кілька додаткових очікувань. Деякі з них мають доступ до бази даних та мережі, що означає справжній введення / виведення. Як я розумію, перемикач потоку - це природний наслідок (хоч і не вимагається) таких очікувань, оскільки початковий потік не встановлює жодного контексту синхронізації, який би визначав, де слід виконувати продовження. Так вони виконуються на нитці пулу ниток. Чи моє міркування неправильне?
1717

@aoven Добрий момент - я не вважав різні види SynchronizationContext- це, безумовно, важливо, оскільки .ContinueWith()використовує SynchronizationContext для відправки продовження; це дійсно би пояснило поведінку, яку ви бачите, якщо awaitвикликається на потоці ThreadPool або потоці ASP.NET. Продовження, безумовно, може бути відправлено на інший потік у цих випадках. З іншого боку, для виклику awaitоднопоточного контексту, такого як контекст WPF Dispecher або Winforms, повинно бути достатньо, щоб забезпечити продовження в оригіналі. нитка
Бен Коттрелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.