Оператор async/await работает неправильно в AspNet-MVC, но работает в консольном приложении

Фон: у меня есть длинный запрос linq к базе данных в моем веб-приложении. Итак, я хочу показать пользователю таймер запроса. Я пытался сделать это с помощью операторов async/await, но застрял. (Я использую signalR для режима реального времени)

Мир моего тестового кода:

MyHub.cs:

public class MyHub : Hub
    {

        public void Message(string message)
        {
            Clients.All.message(message);
        }

        public void Tick(int value)
        {
            Thread.Sleep(1000);
            Clients.Caller.tick(value);
        }

        public void StartAsyncMethod(int value)
        {
            Clients.All.message("<br> M1: StartAsyncMethod started!");
            int t = 0;
            MyAsyncClass myAsyncClass = new MyAsyncClass();
            Task task = myAsyncClass.AsyncMethod(value*1000);
            Clients.All.message("<br> M1: Start While...");

            while (!task.IsCompleted)
            {
                t++;
                Tick(t);
            }

            Clients.All.message("<br> M1: StartAsyncMethod Finished!");

        }
    }

MyAsyncClass.cs

    public class MyAsyncClass
    {
        public async Task AsyncMethod(int _i)
        {
            var hub = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
            hub.Clients.All.message($"<br>==M2== Wait for {_i} milliseconds");
            await Task.Run(() => Sleep(_i));
            hub.Clients.All.message($"<br>==M2== Finish inner method...");
        }

        private void Sleep(int value)
        {
            Thread.Sleep(value);
        }
    }

И дело в while (!task.IsCompleted) — приложение стакается в этом состоянии:

Оператор async/await работает неправильно в AspNet-MVC, но работает в консольном приложении

Если while закомментирован, то работает нормально (но без таймера, конечно):

Оператор async/await работает неправильно в AspNet-MVC, но работает в консольном приложении

Больше чем это - в простом консольном приложении этот код работает в обоих вариантах!

Код:

class Program
    {
        static void Main(string[] args)
        {
            int i = 5;
            Console.WriteLine($"Main : Start StartAsyncMethod({i})");
            StartAsyncMethod(i);
            Console.WriteLine($"Main : FINISH StartAsyncMethod({i})");
            Console.WriteLine("\n\nPress any key to exit....");
            Console.ReadKey();
        }

        public static void StartAsyncMethod(int value)
        {
            Console.WriteLine("  M1 : StartAsyncMethod started!");
            int timer = 0;
            MyAsyncClass myAsyncClass = new MyAsyncClass();
            Task task = myAsyncClass.AsyncMethod(value * 1000);
            //while (!task.IsCompleted)
            //{
            //    timer++;
            //    Thread.Sleep(1000);
            //    Console.WriteLine("  M1 : \"hello\" from While cicle ... ");
            //}
            Console.WriteLine("  M1 : StartAsyncMethod FINISHED!");
        }
    }

    public class MyAsyncClass
    {
        public async Task AsyncMethod(int _i)
        {
            Console.WriteLine($"    M2 : Wait for {_i} milliseconds");
            await Task.Run(() => Sleep(_i));
            Console.WriteLine($"    M2 : Finish inner method...");
        }

        private void Sleep(int v)
        {
            Console.WriteLine($"    M2 : Sleep for {v} mls");
            Thread.Sleep(v);
        }
    }

Выходы:

С while:

Оператор async/await работает неправильно в AspNet-MVC, но работает в консольном приложении

Без while:

Оператор async/await работает неправильно в AspNet-MVC, но работает в консольном приложении

Вопрос: этот код действительно по-разному работает в ASPnet и ConsoleApp, или я что-то пропустил?

Спасибо.

Для тестирования заполнителей не используйте Thread.Sleep, ожидайте Task.Delay, а также не зацикливайтесь на выполненной задаче, это очень беспокоит. Не разгружайте все на задачи, просто await это. вы можете использовать async для методов хаба, await ваши ожидаемые вещи там. однако не обращая внимания на все вышеперечисленное. трудно понять, что вы видите, а что не работает

TheGeneral 08.04.2019 02:25

Спасибо за рекомендацию - для меня это очень ценно!

Alex Ilyin 08.04.2019 17:01

@MichaelRandall, не могли бы вы объяснить, почему зацикливание на завершенной задаче — это плохая практика?

Alex Ilyin 15.04.2019 09:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
924
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это потому, что вы неправильно используете async/await. Обратите внимание, что у вас есть 2 задачи: одна возвращена Task.Run, а другая возвращена AsyncMethod. Зацикливаясь на верхнем уровне Task, вы блокируете поток запросов и продолжение Task.Run, которое

hub.Clients.All.message($"<br>==M2== Finish inner method...");

не может быть выполнен в ASP.NET, поскольку он использует контекст синхронизации, который в случае, если ASP.NET обеспечивает выполнение не более одного потока за раз для потоков, которые совместно используют контекст синхронизации (поток запроса и потоки продолжения совместно используют контекст). Таким образом, вторая задача также не может быть завершена. Консоль не использует контекст синхронизации, поэтому ваши продолжения планируются в пуле потоков. Вам нужно либо использовать ConfigureAwait(flase) с планированием ваших асинхронных задач, либо делать все асинхронно (что более правильно). Вы можете увидеть пример здесь того, как реализовать прогресс для async задач.

Обновлять: Для пояснения цели контекста синхронизации. Давайте представим, что типичная лебедка асинхронного потока запускается асинхронным событием (ввод данных пользователем, входящий запрос и т. д.) и завершается некоторым асинхронным действием (передача данных во внешний источник данных, файл, извлечение данных из веб-ресурса и т. д.). . Асинхронное приложение имеет дело со многими из таких потоков, которые из-за их асинхронной природы могут одновременно запускаться и завершаться, и этот факт среди преимуществ накладывает некоторые подводные камни.

Например, если поток вызывает несколько асинхронных операций, параллелизм их продолжений может быть проблемой. Решением является синхронизация продолжений — здесь вступает в игру контекст синхронизации. Таким образом, в целом он представляет собой некоторую абстракцию для планирования упорядоченного выполнения с помощью двух методов «Отправить» и «Отправить», которые имеют семантику «вызвать и подождать» и «запустить и забыть» соответственно.

Почему бы не просто синхронизировать примитивы? Вкратце контекст синхронизации обеспечивает более общий подход в полусинхронном/полуасинхронном виде, подобно шаблону. Часто ресурсы, используемые для обслуживания асинхронных продолжений, довольно дороги (например, в ОС семейства Windows механизм портов завершения ввода-вывода подразумевает использование специального пула потоков для обслуживания завершенных запросов ввода-вывода), и настоятельно рекомендуется не занимать такие ресурсы дольше, чем необходимого периода времени, поэтому подход «запустить и забыть» часто является предпочтительным способом вместо ожидания объекта синхронизации и блокировки потока, который будет обслуживать другие асинхронные продолжения, а контекст синхронизации обеспечивает абстракцию, которая позволяет эффективно использовать базовую инфраструктуру.

В качестве побочного эффекта некоторых реализаций контекста синхронизации может быть выделена возможность делегировать выполнение некоторого кода из одного потока в другой конкретный поток (например, контексты синхронизации WinForms или WPF), но я бы сказал, что это скорее специфичная для реализации вещь.

На самом деле в контексте ASP.NET нет «основного потока». То, что есть, эксклюзивный ресурс, защищенный контекстом синхронизации, является контекстом запроса. Поскольку очевидно, что несколько запросов обслуживаются одновременно в ASP.NET, и не все они конкурируют за один поток а для выполнения своей логики запроса.

Damien_The_Unbeliever 08.04.2019 08:09

@Damien_The_Unbeliever, конечно, спасибо за указание, я обновил ответ.

Dmytro Mukalov 08.04.2019 09:01

Большое спасибо за ссылку и ответ!

Alex Ilyin 08.04.2019 17:03

@DmytroMukalov, никак не могу разобраться... Перечитал все статьи и примеры, а выдает только вопросы. Какова роль SynchronizationContext?

Alex Ilyin 09.04.2019 12:36

В дополнение к ответу @Dmytro Mukalov я постараюсь ответить на ваш вопрос:

does this code really works in different ways in ASPnet and ConsoleApp, or I have missed something?

Да, безусловно!

Подумайте об этом: веб-клиент обновляется только одним выстрелом через веб-запросы. Это косвенно связано с потоками на сервере.

В консольном приложении выходные сообщения создаются немедленно на консоли, в то время как в ASP.net MVC результаты/сообщения собираются в режиме ожидания (зависит от времени), а затем отправляются обратно клиенту.

Короче говоря, шаблон Coding TAP в ASP.net не транслируется напрямую из консольных приложений. Для более подробного объяснения, пожалуйста, прочитайте:

Асинхронное программирование: введение в Async/Await в ASP.NET

Обычно я рекомендую ссылаться на ответы, на которые вы хотите сослаться, например. "В дополнение к Ответ Дмитрия...". Хотя в настоящее время здесь есть только два ответа, вы никогда не знаете, как ответы на вопрос будут меняться со временем, и будущим посетителям может быть трудно определить «предыдущий ответ», на который вы ссылаетесь.

Damien_The_Unbeliever 08.04.2019 08:07

DaniDev, статья именно то, что я искал! Спасибо!

Alex Ilyin 08.04.2019 17:07

Да, @Stephen Cleary — один из признанных евангелистов/экспертов шаблона TAP. Рад, что помог вам. Это должно дать вам хорошую основу. приветствуется голосование или принятый ответ.

DaniDev 08.04.2019 23:48

Другие вопросы по теме