8 votes

Démarrage et abandon d'une tâche asynchrone dans MVC Action

J'ai une action standard, non asynchrone, du genre :

[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
    PdfGenerator.Current.GenerateAsync(id);
    return Json(null);
}

L'idée étant que je sais que la génération du PDF peut prendre beaucoup de temps, je me contente de lancer la tâche et de revenir, sans me soucier du résultat de l'opération asynchrone.

Dans une application ASP.Net MVC 4 par défaut, cela me donne cette belle exception :

System.InvalidOperationException : Une opération asynchrone ne peut pas être lancée pour le moment. Les opérations asynchrones ne peuvent être lancées qu'au sein d'un gestionnaire ou d'un module asynchrone ou lors de certains événements du cycle de vie de la Page. Si cette exception s'est produite pendant l'exécution d'une page, assurez-vous que la page est marquée <%@ Page Async="true" %>.

Ce qui n'est pas du tout pertinent pour mon scénario. En y regardant de plus près, je peux mettre un drapeau à false pour empêcher cette exception :

<appSettings>
    <!-- Allows throwaway async operations from MVC Controller actions -->
    <add key="aspnet:AllowAsyncDuringSyncStages" value="true" />
</appSettings>

https://stackoverflow.com/a/15230973/176877
http://msdn.microsoft.com/en-us/library/hh975440.aspx

Mais la question est la suivante : y a-t-il un inconvénient à lancer cette opération asynchrone et à l'oublier à partir d'une action synchrone du contrôleur MVC ? Tout ce que j'ai pu trouver recommande de rendre le contrôleur asynchrone, mais ce n'est pas ce que je recherche - cela ne servirait à rien puisqu'il devrait toujours revenir immédiatement.

6voto

Nikola Bogdanović Points 1628

Détendez-vous, comme le dit Microsoft lui-même ( http://msdn.microsoft.com/en-us/library/system.web.httpcontext.allowasyncduringsyncstages.aspx ):

Ce comportement est censé être un filet de sécurité pour permettre à la population de se protéger. vous écrivez du code asynchrone qui ne correspond pas aux modèles attendus et qui pourrait avoir des effets secondaires négatifs.

Il suffit de se rappeler quelques règles simples :

  • Ne jamais attendre à l'intérieur (asynchrone ou non) d'événements void (car ils reviennent immédiatement). Certains événements de page WebForms supportent des attentes simples à l'intérieur d'eux - mais RegisterAsyncTask est toujours l'approche privilégiée.

  • N'attendez pas sur les méthodes async void (car elles retournent immédiatement).

  • N'attendez pas de façon synchrone dans le thread de l'interface graphique ou de la requête ( .Wait() , .Result() , .WaitAll() , WaitAny() ) sur les méthodes asynchrones qui ne disposent pas de .ConfigureAwait(false) sur la racine attendue à l'intérieur de ceux-ci, ou leur racine Task n'est pas lancé avec .Run() ou n'ont pas la TaskScheduler.Default explicitement spécifié (car l'interface graphique ou la demande se bloqueront ainsi).

  • Utilice .ConfigureAwait(false) o Task.Run ou spécifier explicitement TaskScheduler.Default pour chaque processus d'arrière-plan, et dans chaque méthode de bibliothèque, qui fait no doivent continuer sur le contexte de synchronisation - pensez-y comme au "thread appelant", mais sachez qu'il n'en est pas un (et pas toujours sur le même), et peut même ne plus exister (si la requête s'est déjà terminée). Ce seul fait permet d'éviter la plupart des erreurs courantes d'asynchronisme et d'attente, et augmente également les performances.

Microsoft a simplement supposé que vous aviez oublié d'attendre votre tâche...

UPDATE : Comme Stephen l'a clairement indiqué dans sa réponse (sans jeu de mots), il existe un danger inhérent mais caché avec toutes les formes de " fire and forget " lorsque l'on travaille avec des pools d'applications, qui n'est pas uniquement spécifique à async/await, mais aussi à Tasks, ThreadPool et à toutes les autres méthodes de ce type. no La fin de la demande est garantie (le pool d'applications peut se recycler à tout moment pour un certain nombre de raisons).

Vous pouvez vous en préoccuper ou non (s'il ne s'agit pas d'un élément critique pour l'entreprise, comme dans le cas particulier de l'OP), mais vous devez toujours en être conscient.

2voto

Stephen Cleary Points 91731

En InvalidOperationException n'est pas un avertissement. AllowAsyncDuringSyncStages est une situation dangereuse et que je voudrais personnellement jamais utiliser.

En correct La solution consiste à stocker la demande dans une file d'attente persistante (par exemple, une file d'attente Azure) et à faire traiter cette file d'attente par une application distincte (par exemple, un rôle de travailleur Azure). C'est beaucoup plus de travail, mais c'est la façon correcte de procéder. Je veux dire "correcte" dans le sens où le recyclage de votre application par IIS/ASP.NET ne perturbera pas votre traitement.

Si vous absolument vous voulez garder votre traitement en mémoire (et, comme corollaire, vous êtes Accepter de "perdre" occasionnellement des demandes. ), alors au moins enregistrez le travail avec ASP.NET. J'ai code source sur mon blog que vous pouvez déposer dans votre solution pour faire cela. Mais s'il vous plaît, ne vous contentez pas de saisir le code ; lisez tout le message pour comprendre pourquoi ce n'est toujours pas la meilleure solution :)

1voto

Chris Moschini Points 7278

La réponse s'avère être un peu plus compliquée :

Si ce que vous faites, comme dans mon exemple, est simplement de mettre en place une tâche asynchrone de longue durée et de la renvoyer, vous n'avez pas besoin de faire plus que ce que j'ai indiqué dans ma question.

Mais, il y a un risque : Si quelqu'un étend cette action plus tard et qu'il est logique que l'action soit asynchrone, alors la méthode asynchrone "fire and forget" qu'elle contient va réussir ou échouer de manière aléatoire. Cela se passe comme suit :

  1. La méthode du feu et de l'oubli se termine.
  2. Comme elle a été déclenchée à l'intérieur d'une tâche asynchrone, elle tentera de rejoindre le contexte de cette tâche ("marshal") lors de son retour.
  3. Si l'action asynchrone du contrôleur s'est terminée et que l'instance du contrôleur a depuis été récupérée, le contexte de la tâche sera désormais nul.

Le fait qu'elle soit nulle ou non varie en fonction des délais mentionnés ci-dessus - parfois elle l'est, parfois elle ne l'est pas. Cela signifie qu'un développeur peut tester et trouver que tout fonctionne correctement, passer en production et tout explose. Pire, l'erreur que cela provoque est :

  1. Une NullReferenceException - très vague.
  2. Introduit dans du code .Net Framework que vous ne pouvez même pas pénétrer dans Visual Studio - généralement System.Web.dll.
  3. Il n'est pas capturé par un try/catch car la partie de la bibliothèque parallèle des tâches qui vous permet de revenir dans des contextes try/catch existants est la partie qui échoue.

Ainsi, vous obtiendrez une erreur mystérieuse où les choses ne se produisent tout simplement pas - des exceptions sont lancées, mais vous n'en avez probablement pas connaissance. Ce n'est pas bon.

La façon propre d'éviter cela est :

[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
    #pragma warning disable 4014 // Fire and forget.
    Task.Run(async () =>
    {
        await PdfGenerator.Current.GenerateAsync(id);
    }).ConfigureAwait(false);

    return Json(null);
}

Nous avons donc ici un contrôleur synchrone sans problème - mais pour nous assurer qu'il ne le sera pas même si nous le changeons en asynchrone plus tard, nous démarrons explicitement une nouvelle tâche via Run, qui par défaut place la tâche sur le ThreadPool principal. Si nous l'attendions, il tenterait de la rattacher à ce contexte, ce que nous ne voulons pas - nous ne l'attendons donc pas, et cela nous vaut un avertissement gênant. Nous désactivons cet avertissement avec le pragma warning disable.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X