Фон: у меня есть длинный запрос 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)
— приложение стакается в этом состоянии:
Если while
закомментирован, то работает нормально (но без таймера, конечно):
Больше чем это - в простом консольном приложении этот код работает в обоих вариантах!
Код:
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
:
Без while
:
Вопрос: этот код действительно по-разному работает в ASPnet и ConsoleApp, или я что-то пропустил?
Спасибо.
Спасибо за рекомендацию - для меня это очень ценно!
@MichaelRandall, не могли бы вы объяснить, почему зацикливание на завершенной задаче — это плохая практика?
Это потому, что вы неправильно используете 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, конечно, спасибо за указание, я обновил ответ.
Большое спасибо за ссылку и ответ!
@DmytroMukalov, никак не могу разобраться... Перечитал все статьи и примеры, а выдает только вопросы. Какова роль SynchronizationContext?
В дополнение к ответу @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
Обычно я рекомендую ссылаться на ответы, на которые вы хотите сослаться, например. "В дополнение к Ответ Дмитрия...". Хотя в настоящее время здесь есть только два ответа, вы никогда не знаете, как ответы на вопрос будут меняться со временем, и будущим посетителям может быть трудно определить «предыдущий ответ», на который вы ссылаетесь.
DaniDev, статья именно то, что я искал! Спасибо!
Да, @Stephen Cleary — один из признанных евангелистов/экспертов шаблона TAP. Рад, что помог вам. Это должно дать вам хорошую основу. приветствуется голосование или принятый ответ.
Для тестирования заполнителей не используйте
Thread.Sleep
, ожидайтеTask.Delay
, а также не зацикливайтесь на выполненной задаче, это очень беспокоит. Не разгружайте все на задачи, простоawait
это. вы можете использоватьasync
для методов хаба,await
ваши ожидаемые вещи там. однако не обращая внимания на все вышеперечисленное. трудно понять, что вы видите, а что не работает