J'ai un très simple contrôleur ASP.NET MVC 4:
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);
}
}
Lorsque je démarre le projet, ma vue s’affiche et l’apparence est bonne, mais lors de la mise à jour de la page, l’erreur suivante apparaît:
[InvalidOperationException: un module ou un gestionnaire asynchrone s'est terminé alors qu'une opération asynchrone était toujours en attente.]
Pourquoi ça se passe? J'ai fait quelques tests:
task = DownloadAsync();
du constructeur et le plaçons dans la méthode Index
, tout fonctionnera sans les erreurs.DownloadAsync()
corps return 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
Dans un constructeur du contrôleur?
Dans Async Void, ASP.Net et nombre d'opérations en attente , Stephan Cleary explique la racine de cette erreur:
Historiquement, ASP.NET prenait en charge les opérations asynchrones propres depuis .NET 2.0 via le modèle asynchrone basé sur des événements (EAP), dans lequel des composants asynchrones notifiaient au SynchronizationContext leur démarrage et leur achèvement.
Ce qui se passe, c’est que vous lancez DownloadAsync
à l’intérieur du constructeur de votre classe, où à l'intérieur de vous await
lors de l'appel http asynchrone. Cela enregistre l'opération asynchrone avec ASP.NET SynchronizationContext
. Lorsque votre HomeController
retourne, il voit qu’une opération asynchrone en attente n’a pas encore été terminée et qu’il déclenche une exception.
Si nous supprimons task = DownloadAsync (); à partir du constructeur et le mettre dans la méthode Index cela fonctionnera bien sans les erreurs.
Comme je l'ai expliqué ci-dessus, c'est parce qu'il n'y a plus d'opération asynchrone en attente lorsque vous revenez du contrôleur.
Si nous utilisons un autre retour de corps DownloadAsync (), attendez
Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });
, cela fonctionnera correctement.
C'est parce que Task.Factory.StartNew
fait quelque chose de dangereux dans ASP.NET. Il n'enregistre pas l'exécution des tâches avec ASP.NET. Cela peut entraîner des cas Edge dans lesquels un recyclage de pool s'exécute, en ignorant complètement votre tâche en arrière-plan, ce qui provoque un abandon anormal. C'est pourquoi vous devez utiliser un mécanisme qui enregistre la tâche, tel 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 soit HostingEnvironment
(si vous êtes sur .NET 4.5.2) ou BackgroundTaskManager
. Notez qu'en faisant cela, vous utilisez un thread threadpool pour effectuer async IO opérations, ce qui est redondant et exactement ce que async IO avec async-await
tente de vaincre.
ASP.NET considère qu'il est illégal de démarrer une "opération asynchrone" liée à son SynchronizationContext
et de retourner un ActionResult
avant la fin de toutes les opérations démarrées. Toutes les méthodes async
s'enregistrent comme "opérations asynchrones". Vous devez donc vous assurer que tous les appels de ce type qui se lient à ASP.NET SynchronizationContext
sont terminés avant de renvoyer un ActionResult
.
Dans votre code, vous revenez sans vous assurer que DownloadAsync()
est terminé. Cependant, vous enregistrez le résultat dans le membre task
. Il est donc très facile de s’assurer que tout est complet. Mettez simplement await task
Dans toutes vos méthodes d'action (après les avoir asyncifiées) avant de renvoyer:
public async Task<ActionResult> IndexAsync()
{
try
{
return View();
}
finally
{
await task;
}
}
MODIFIER:
Dans certains cas, vous devrez peut-être appeler une méthode async
qui ne doit pas être terminée avant de retourner dans ASP.NET . Par exemple, vous souhaiterez peut-être initialiser paresseusement une tâche de service en arrière-plan qui devrait continuer à s'exécuter une fois la requête en cours terminée. Ce n'est pas le cas pour le code de l'OP, car celui-ci souhaite que la tâche soit terminée avant son retour. Toutefois, si vous devez démarrer sans attendre une tâche, vous pouvez le faire. Vous devez simplement utiliser une technique pour "échapper" au courant SynchronizationContext.Current
.
( non recommencé ) Une fonction de Task.Run()
consiste à échapper au contexte de synchronisation actuel. Toutefois, il est déconseillé de l'utiliser dans ASP.NET car le pool de threads d'ASP.NET est spécial. En outre, même en dehors de ASP.NET, cette approche entraîne 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 ni déranger immédiatement le threadpool d'ASP.NET consiste à définir SynchronizationContext.Current
À null
, appelez votre méthode async
, puis restaurez la valeur d'origine .
J'ai rencontré un problème connexe. Un client utilise une interface qui renvoie une tâche et est implémentée avec async.
Dans Visual Studio 2015, la méthode cliente qui est asynchrone et qui n'utilise pas le mot clé wait lors de l'appel de la méthode ne reçoit aucun avertissement ou erreur, le code est compilé proprement. Une condition de concurrence est promue en production.
La méthode myWebClient.DownloadStringTaskAsync s'exécute sur un thread distinct et ne bloque pas. Une solution possible consiste à faire cela avec le gestionnaire d'événements DownloadDataCompleted 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();
}
La méthode return async Task
Et ConfigureAwait(false)
peuvent être l'une des solutions. Il agira comme un vide asynchrone et ne poursuivra pas le contexte de synchronisation (tant que vous ne concernerez pas le résultat final de la méthode)
J'ai eu un problème similaire, mais a été résolu en passant un CancellationToken en tant que paramètre à la méthode async.