53 votes

Exécution d'une opération asynchrone déclenchée par une requête de page Web ASP.NET

J'ai une opération asynchrone qui, pour diverses raisons, doit être déclenchée par un appel HTTP à une page Web ASP.NET. Lorsque ma page est demandée, elle doit lancer cette opération et renvoyer immédiatement un accusé de réception au client.

Cette méthode est également exposée via un service web WCF, et elle fonctionne parfaitement.

Lors de ma première tentative, une exception a été levée, me disant :

Asynchronous operations are not allowed in this context.
Page starting an asynchronous operation has to have the Async
attribute set to true and an asynchronous operation can only be
started on a page prior to PreRenderComplete event.

Alors bien sûr, j'ai ajouté le Async="true" au paramètre @Page directive. Maintenant, je ne reçois pas d'erreur, mais la page se bloque jusqu'à ce que l'opération asynchrone soit terminée.

Comment faire pour qu'une véritable page "fire-and-forget" fonctionne ?

Edit : Quelques codes pour plus d'informations. C'est un peu plus compliqué que ça, mais j'ai essayé d'y mettre l'idée générale.

public partial class SendMessagePage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string message = Request.QueryString["Message"];
        string clientId = Request.QueryString["ClientId"];

        AsyncMessageSender sender = new AsyncMessageSender(clientId, message);
        sender.Start();

        Response.Write("Success");
    }
}

La classe AsyncMessageSender :

public class AsyncMessageSender
{
    private BackgroundWorker backgroundWorker;
    private string client;
    private string msg;

    public AsyncMessageSender(string clientId, string message)
    {
        this.client = clientId;
        this.msg = message;

        // setup background thread to listen
        backgroundThread = new BackgroundWorker();
        backgroundThread.WorkerSupportsCancellation = true;
        backgroundThread.DoWork += new DoWorkEventHandler(backgroundThread_DoWork);
    }

    public void Start()
    {
        backgroundThread.RunWorkerAsync();
    }

    ...
    // after that it's pretty predictable
}

33voto

vikingben Points 422

Si vous utilisez des formulaires Web, définissez Ansync = "true" dans votre page .aspx où vous effectuez la demande.
<%@ Page Language="C#" Async="true" ... %>

31voto

Ilya Tchivilev Points 546

Si vous n'avez pas l'intention de renvoyer quoi que ce soit à l'utilisateur, vous pouvez simplement lancer un thread séparé ou, pour une approche plus rapide et plus sale, utiliser un délégué et l'invoquer de manière asynchrone. Si vous ne vous souciez pas de notifier l'utilisateur lorsque la tâche asynchrone se termine, vous pouvez ignorer le callback. Essayez de placer un point d'arrêt à la fin de la méthode SomeVeryLongAction(), et vous verrez qu'elle se termine après que la page a déjà été servie :

private delegate void DoStuff(); //delegate for the action

protected void Page_Load(object sender, EventArgs e)
{

}

protected void Button1_Click(object sender, EventArgs e)
{
    //create the delegate
    DoStuff myAction = new DoStuff(SomeVeryLongAction); 
    //invoke it asynchrnously, control passes to next statement
    myAction.BeginInvoke(null, null);
    Button1.Text = DateTime.Now.ToString();
}

private void SomeVeryLongAction()
{
    for (int i = 0; i < 100; i++)
    {
        //simulation of some VERY long job
        System.Threading.Thread.Sleep(100);
    }
}

27voto

Charlie Flowers Points 9145

OK, voici le problème : l'attribut Async est destiné au cas où votre page va appeler une tâche de longue durée qui bloque également le thread, et où votre page a besoin de la sortie de cette tâche pour renvoyer des informations à l'utilisateur. Par exemple, si votre page doit appeler un service Web, attendre sa réponse, puis utiliser les données de la réponse pour rendre votre page.

Si vous utilisez l'attribut Async, c'est pour éviter de bloquer le fil d'exécution. C'est important car les applications ASP.NET utilisent un pool de threads pour servir les requêtes, et il n'y a qu'un nombre relativement faible de threads disponibles. Et si chaque appel bloque le thread pendant qu'il attend l'appel du service Web, vous allez bientôt atteindre un nombre suffisant d'utilisateurs simultanés pour que les utilisateurs doivent attendre la fin de ces appels de service Web. L'attribut Async permet au thread de retourner dans le pool de threads et de servir d'autres visiteurs simultanés de votre site Web, plutôt que de le forcer à rester immobile sans rien faire en attendant le retour de l'appel au service Web.

Le résultat pour vous est le suivant : l'attribut Async est conçu pour le cas où vous ne pouvez pas rendre la page avant que la tâche asynchrone ne soit terminée, et c'est pourquoi il ne rend pas la page immédiatement.

Vous devez lancer votre propre thread, et en faire un thread démon. Je ne me souviens pas de la syntaxe exacte pour cela, mais vous pouvez facilement la trouver dans la doc en cherchant "daemon" dans la doc BCL. Cela signifie que le thread empêchera votre application de s'arrêter pendant qu'il est en vie, ce qui est important car ASP.NET et IIS se réservent le droit de "recycler votre processus" quand ils le jugent nécessaire, et si cela se produit pendant que votre thread travaille, votre tâche sera arrêtée. Faire du thread un démon empêchera cela (à l'exception de quelques rares cas limites ... vous en saurez plus lorsque vous trouverez la documentation à ce sujet).

C'est dans ce fil de démon que vous lancerez ces tâches. Et une fois que vous avez demandé au thread démon d'effectuer la tâche, vous pouvez immédiatement rendre votre page ... le rendu de la page se fera immédiatement.

Cependant, il serait encore mieux d'implémenter un service Windows pour effectuer cette tâche plutôt qu'un fil démon dans votre processus ASP.NET. Demandez à votre application ASP.NET de communiquer la tâche à exécuter au service. Vous n'avez pas besoin d'un fil démon et vous n'avez pas à vous inquiéter du recyclage de votre processus ASP.NET. Comment demander au service d'effectuer la tâche ? Peut-être via WCF, ou peut-être en insérant un enregistrement dans une table de base de données que le service interroge. Ou encore de plusieurs autres façons.

EDIT : Voici une autre idée, que j'ai déjà utilisée dans le même but. Ecrivez les informations sur votre tâche dans une file d'attente MSMQ. Demandez à un autre processus (peut-être même sur une autre machine) de tirer de cette file d'attente et d'effectuer la tâche qui prend du temps. Le travail d'insertion dans une file d'attente est optimisé pour revenir aussi vite que possible, de sorte que votre thread ne se bloque pas pendant que les données que vous mettez dans la file d'attente sont envoyées à travers le fil ou quelque chose comme ça. C'est l'un des moyens les plus rapides de signaler qu'une tâche doit être effectuée sans attendre que cette tâche soit exécutée.

3voto

Serge Points 2386

Vous pouvez contourner cette limitation assez facilement et sans même définir Async à true.

public void Start()
{
    new Task(() =>
    {
        backgroundThread.RunWorkerAsync();
    }).Start();
}

1voto

Amrik Points 28

Si vous obtenez cette erreur lorsque vous appelez un service web de manière asynchrone, assurez-vous d'ajouter l'attribut Async='true' comme indiqué par l'instruction message d'exception ?

haut de la page < Page Language='VB' Async='true' AutoEventWireup='false' CodeFile='mynewpage.aspx.vb' Inherits='mynewpage' %>

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