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:

  1. si nous retirons task = DownloadAsync(); du constructeur et le mettons dans la méthode Index il fonctionnera très bien sans les erreurs.
  2. si nous utilisons un autre 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 à l'intérieur d'un constructeur du contrôleur?

31
demandé sur Yuval Itzchakov 2015-03-02 11:56:33

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.

39
répondu Yuval Itzchakov 2017-05-23 11:47:14

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.

2
répondu Beans 2016-05-17 20:18:32

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 to null , appelez votre méthode async , puis restaurez la valeur originale .

2
répondu binki 2018-01-17 20:55:49

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();
}
1
répondu Dan Randolph 2017-05-05 01:51:42

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)

0
répondu code4j 2017-08-24 07:40:44

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();
        }
 }
0
répondu Mohd Aijaz 2017-08-29 09:18:19