ASP.NET contrôleur: module ou manipulateur asynchrone terminé alors qu'une opération asynchrone était toujours en attente
j'ai un très simple ASP.NET MVC 4 controller:
public class HomeController : Controller
{
private const string MY_URL = "http://smthing";
private readonly Task<string> task;
public HomeController() { task = DownloadAsync(); }
public ActionResult Index() { return View(); }
private async Task<string> DownloadAsync()
{
using (WebClient myWebClient = new WebClient())
return await myWebClient.DownloadStringTaskAsync(MY_URL)
.ConfigureAwait(false);
}
}
quand je démarre le projet je vois ma vue et il semble bien, mais quand je mets à jour la page je reçois l'erreur suivante:
[InvalidOperationException: asynchrone module ou du gestionnaire terminé pendant une opération asynchrone est toujours en attente.]
pourquoi ça arrive? J'ai fait quelques tests:
- si nous retirons
task = DownloadAsync();
du constructeur et le mettons dans la méthodeIndex
il fonctionnera très bien sans les erreurs. - si nous utilisons un autre
DownloadAsync()
corpsreturn await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });
il fonctionnera correctement.
Pourquoi est-il impossible d'utiliser la méthode WebClient.DownloadStringTaskAsync
à l'intérieur d'un constructeur du contrôleur?
6 réponses
In async Void, ASP.Net , et compte des opérations en cours , Stephan Cleary explique la racine de cette erreur:
historiquement, ASP.NET a pris en charge des opérations asynchrones et propres depuis .NET 2.0 via le modèle asynchrone basé sur L'événement (EAP), en quels composants asynchrones notifient le contexte de synchronisation leur début et de fin.
ce qui se passe est que vous tirez DownloadAsync
à l'intérieur de votre constructeur de classe, où à l'intérieur de vous await
sur l'appel http async. Ceci enregistre le fonctionnement asynchrone avec le ASP.NET SynchronizationContext
. Lorsque votre HomeController
retourne, il voit qu'il a une opération asynchrone en attente qui n'a pas encore terminé, et c'est pourquoi il soulève une exception.
si nous supprimons task = DownloadAsync (); du constructeur et le mettons dans la méthode de l'Indice, il fonctionnera sans erreurs.
comme je l'ai expliqué ci-dessus, c'est parce que vous n'avez plus une opération asynchrone en attente qui se déroule alors que vous revenez du contrôleur.
si nous utilisons un autre DownloadAsync () retour du corps attendre
Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });
il fonctionnera correctement.
c'est parce que Task.Factory.StartNew
fait quelque chose de dangereux en ASP.NET. Ce n'est pas le cas. enregistrer l'exécution des tâches avec ASP.NET. Cela peut conduire à des cas extrêmes où un pool recycle exécute, ignorant complètement votre tâche de fond, provoquant un abandon anormal. C'est pourquoi vous devez utiliser un mécanisme qui enregistre la tâche, telles que HostingEnvironment.QueueBackgroundWorkItem
.
c'est pourquoi il n'est pas possible de faire ce que vous faites, comme vous le faites. Si vous voulez vraiment que cela s'exécute dans un thread d'arrière-plan, dans un style" fire-and-forget", utilisez l'un ou l'autre HostingEnvironment
(si vous êtes sur .NET 4.5.2) ou BackgroundTaskManager
. Notez qu'en faisant cela, vous utilisez un ThreadPool thread pour effectuer des opérations asynchrones, ce qui est redondant et exactement ce que asynchrone avec async-await
tente de surmonter.
j'ai rencontré un problème connexe. Un client utilise une Interface qui retourne la tâche et est implémentée avec async.
dans Visual Studio 2015, la méthode client qui est async et qui n'utilise pas le mot-clé attente lors de l'invocation de la méthode ne reçoit aucun avertissement ou erreur, le code se compile proprement. Une condition de race est promue à la production.
ASP.NET considère qu'il est illégal de commencer une "opération asynchrone" liée à son SynchronizationContext
et de retourner un ActionResult
avant que toutes les opérations commencées soient terminées. Toutes les méthodes async
s'enregistrent comme des "opérations asynchrones", vous devez donc vous assurer que tous ces appels qui se lient à la ASP.NET SynchronizationContext
terminé avant de retourner un ActionResult
.
dans votre code, vous retournez sans vous assurer que DownloadAsync()
a été exécuté. Cependant, vous enregistrez le résultat au membre task
, de sorte qu'il est très facile de s'assurer qu'il est complet. Mettez simplement await task
dans toutes vos méthodes d'action (après les avoir asynciquées) avant de retourner:
public async Task<ActionResult> IndexAsync()
{
try
{
return View();
}
finally
{
await task;
}
}
EDIT:
dans certains cas, vous pouvez avoir besoin d'appeler un async
méthode que ne devrait pas compléter avant de retourner à ASP.NET . Par exemple, vous pouvez vouloir initialiser paresseusement un service d'arrière-plan tâche qui devrait continuer à fonctionner après la fin de la requête actuelle. Ce n'est pas le cas pour le code de L'OP parce que l'OP veut que la tâche soit terminée avant le retour. Cependant, si vous avez besoin de commencer et de ne pas attendre une tâche, il y a un moyen de le faire. Vous devez simplement utiliser une technique pour "échapper" à l'actuel SynchronizationContext.Current
.
-
( non repris ) une caractéristique de
Task.Run()
est d'échapper au courant contexte de synchronisation. Cependant, les gens recommandent de ne pas utiliser ce ASP.NET parce que ASP.NET le threadpool est spécial. En outre, même à l'extérieur de ASP.NET, cette approche se traduit par un changement de contexte supplémentaire. -
( recommandé ) un moyen sûr d'échapper au contexte de synchronisation actuel sans forcer un commutateur de contexte supplémentaire ou déranger ASP.NET 's threadpool immediately is to set
SynchronizationContext.Current
tonull
, appelez votre méthodeasync
, puis restaurez la valeur originale .
la méthode myWebClient.DownloadStringTaskAsync fonctionne sur un thread séparé et ne bloque pas. Une solution possible est de le faire avec le Gestionnaire D'événements téléchargeable pour myWebClient et un champ de classe SemaphoreSlim.
private SemaphoreSlim signalDownloadComplete = new SemaphoreSlim(0, 1);
private bool isDownloading = false;
....
//Add to DownloadAsync() method
myWebClient.DownloadDataCompleted += (s, e) => {
isDownloading = false;
signalDownloadComplete.Release();
}
isDownloading = true;
...
//Add to block main calling method from returning until download is completed
if (isDownloading)
{
await signalDownloadComplete.WaitAsync();
}
méthode retour async Task
, et ConfigureAwait(false)
peut être l'un de la solution. Il agira comme async void et ne continuera pas avec le contexte sync (tant que vous ne vous souciez pas vraiment du résultat final de la méthode)
exemple de notification par courriel avec pièce jointe ..
public async Task SendNotification(string SendTo,string[] cc,string subject,string body,string path)
{
SmtpClient client = new SmtpClient();
MailMessage message = new MailMessage();
message.To.Add(new MailAddress(SendTo));
foreach (string ccmail in cc)
{
message.CC.Add(new MailAddress(ccmail));
}
message.Subject = subject;
message.Body =body;
message.Attachments.Add(new Attachment(path));
//message.Attachments.Add(a);
try {
message.Priority = MailPriority.High;
message.IsBodyHtml = true;
await Task.Yield();
client.Send(message);
}
catch(Exception ex)
{
ex.ToString();
}
}