Comment appeler en toute sécurité une méthode asynchrone en C # sans attendre


322

J'ai une asyncméthode qui ne renvoie aucune donnée:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

J'appelle cela à partir d'une autre méthode qui renvoie des données:

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

Appeler MyAsyncMethod()sans l'attendre provoque un avertissement « Parce que cet appel n'est pas attendu, la méthode actuelle continue de s'exécuter avant la fin de l'appel » dans Visual Studio. Sur la page de cet avertissement, il est indiqué:

Vous ne devez envisager de supprimer l'avertissement que si vous êtes sûr de ne pas attendre la fin de l'appel asynchrone et que la méthode appelée ne déclenche aucune exception .

Je suis sûr que je ne veux pas attendre la fin de l'appel; Je n'en ai pas besoin ni le temps. Mais l'appel pourrait soulever des exceptions.

Je suis tombé sur ce problème à plusieurs reprises et je suis sûr que c'est un problème commun qui doit avoir une solution commune.

Comment appeler en toute sécurité une méthode asynchrone sans attendre le résultat?

Mettre à jour:

Pour les personnes suggérant que j'attends juste le résultat, il s'agit d'un code qui répond à une requête Web sur notre service Web (API Web ASP.NET). L'attente dans un contexte d'interface utilisateur laisse le thread d'interface utilisateur libre, mais l'attente dans un appel de demande Web attendra que la tâche se termine avant de répondre à la demande, augmentant ainsi les temps de réponse sans raison.


Pourquoi ne pas simplement créer une méthode d'achèvement et l'ignorer simplement là-bas? Parce que s'il s'exécute sur le thread d'arrière-plan. Cela n'empêchera pas votre programme de se terminer de toute façon.
Yahya

1
Si vous ne voulez pas attendre le résultat, la seule option est d'ignorer / supprimer l'avertissement. Si vous ne voulez attendre le résultat / exception puisMyAsyncMethod().Wait()
Peter Ritchie

1
À propos de votre montage: cela n'a pas de sens pour moi. Supposons que la réponse soit envoyée au client 1 seconde après la demande et 2 secondes plus tard, votre méthode asynchrone lève une exception. Que feriez-vous avec cette exception? Vous ne pouvez pas l'envoyer au client, si votre réponse est déjà envoyée. Que feriez-vous d'autre?

2
@Romoku Assez juste. En supposant que quelqu'un regarde le journal, de toute façon. :)

2
Une variante du scénario de l'API Web ASP.NET est une API Web auto-hébergée dans un processus de longue durée (comme, par exemple, un service Windows), où une demande crée une tâche d'arrière-plan longue pour faire quelque chose de cher, mais qui veut toujours obtenir une réponse rapidement avec un HTTP 202 (accepté).
David Rubin

Réponses:


187

Si vous voulez obtenir l'exception "asynchrone", vous pouvez faire:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

Cela vous permettra de traiter une exception sur un thread autre que le thread "principal". Cela signifie que vous n'avez pas à "attendre" l'appel MyAsyncMethod()depuis le thread qui appelle MyAsyncMethod; mais, vous permet toujours de faire quelque chose avec une exception - mais seulement si une exception se produit.

Mettre à jour:

techniquement, vous pourriez faire quelque chose de similaire avec await:

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

... ce qui serait utile si vous aviez besoin d'utiliser spécifiquement try/ catch(ou using) mais je trouve que ContinueWithc'est un peu plus explicite parce que vous devez savoir ce que cela ConfigureAwait(false)signifie.


7
J'ai transformé ceci en une méthode d'extension sur Task: classe statique publique AsyncUtility {public static void PerformAsyncTaskWithoutAwait (cette tâche, Action <Task> exceptionHandler) {var dummy = task.ContinueWith (t => exceptionHandler (t), TaskContinuationOptions.OnlyOnFaulted); }} Utilisation: MyAsyncMethod (). PerformAsyncTaskWithoutAwait (t => log.ErrorFormat ("Une erreur s'est produite lors de l'appel de MyAsyncMethod: \ n {0}", t.Exception));
Mark Avenius

2
downvoter. Commentaires? S'il y a quelque chose de mal dans la réponse, j'aimerais savoir et / ou corriger.
Peter Ritchie

2
Hé - je n'ai pas downvote, mais ... Pouvez-vous expliquer votre mise à jour? L'appel n'était pas censé être attendu, donc si vous le faites comme ça, alors vous attendez l'appel, vous ne continuez simplement pas sur le contexte capturé ...
Bartosz

1
"attendre" n'est pas la description correcte dans ce contexte. La ligne après le ConfiguratAwait(false)ne s'exécute pas tant que la tâche n'est pas terminée, mais le thread actuel n'attend pas (c'est-à-dire le bloc) pour cela, la ligne suivante est appelée de manière asynchrone à l'appel en attente. Sans ConfigureAwait(false)cela, la ligne suivante serait exécutée dans le contexte de la demande Web d'origine. Avec ConfigurateAwait(false)elle, elle est exécutée dans le même contexte que la méthode async (tâche), libérant le contexte / thread d'origine pour continuer ...
Peter Ritchie

15
La version ContinueWith n'est pas la même que la version try {wait} catch {}. Dans la première version, tout ce qui se trouve après ContinueWith () s'exécutera immédiatement. La tâche initiale est renvoyée et oubliée. Dans la deuxième version, tout ce qui se trouve après le catch {} ne s'exécutera qu'une fois la tâche initiale terminée. La deuxième version équivaut à "attendre MyAsyncMethod (). ContinueWith (t => Console.WriteLine (t.Exception), TaskContinuationOptions.OnlyOnFaulted) .ConfigureAwait (fals);
Thanasis Ioannidis

67

Vous devez d'abord envisager de créer GetStringDataune asyncméthode et de lui faire awaitrenvoyer la tâche MyAsyncMethod.

Si vous êtes absolument sûr que vous n'avez pas besoin de gérer les exceptions MyAsyncMethod ou de savoir quand cela se termine, vous pouvez le faire:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

BTW, ce n'est pas un "problème commun". Il est très rare de vouloir exécuter du code sans se soucier s'il se termine et sans se soucier s'il se termine avec succès.

Mettre à jour:

Puisque vous êtes sur ASP.NET et que vous souhaitez revenir tôt, vous pouvez trouver mon article de blog sur le sujet utile . Cependant, ASP.NET n'a pas été conçu pour cela et il n'y a aucune garantie que votre code s'exécutera après le retour de la réponse. ASP.NET fera de son mieux pour le laisser fonctionner, mais il ne peut pas le garantir.

Donc, c'est une bonne solution pour quelque chose de simple comme jeter un événement dans un journal où cela n'a pas vraiment d' importance si vous en perdez quelques-uns ici et là. Ce n'est pas une bonne solution pour tout type d'opérations critiques pour l'entreprise. Dans ces situations, vous devez adopter une architecture plus complexe, avec un moyen persistant pour enregistrer les opérations (par exemple, Azure Queues, MSMQ) et un processus d'arrière-plan distinct (par exemple, Azure Worker Role, Win32 Service) pour les traiter.


2
Je pense que vous avez peut-être mal compris. Je fais des soins si elle émet des exceptions et échoue, mais je ne veux pas avoir à attendre la méthode avant de retourner mes données. Voir également mon montage sur le contexte dans lequel je travaille si cela fait une différence.
George Powell

5
@GeorgePowell: Il est très dangereux de faire exécuter du code dans un contexte ASP.NET sans demande active. J'ai un article de blog qui peut vous aider , mais sans en savoir plus sur votre problème, je ne peux pas dire si je recommanderais cette approche ou non.
Stephen Cleary

@StephenCleary J'ai un besoin similaire. Dans mon exemple, j'ai / j'ai besoin d'un moteur de traitement par lots pour fonctionner dans le cloud, je vais "cingler" le point final pour lancer le traitement par lots, mais je veux y retourner immédiatement. Depuis le ping, il démarre, il peut tout gérer à partir de là. S'il y a des exceptions qui sont levées, alors elles seront simplement enregistrées dans mes tables "BatchProcessLog / Error" ...
ganders

8
En C # 7, vous pouvez remplacer var _ = MyAsyncMethod();par _ = MyAsyncMethod();. Cela évite toujours l'avertissement CS4014, mais cela rend un peu plus explicite que vous n'utilisez pas la variable.
Brian

48

La réponse de Peter Ritchie était ce que je voulais, et l'article de Stephen Cleary sur le retour précoce dans ASP.NET a été très utile.

Cependant, en tant que problème plus général (non spécifique à un contexte ASP.NET), l'application console suivante montre l'utilisation et le comportement de la réponse de Peter en utilisant Task.ContinueWith(...)

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData()retourne tôt sans attendre MyAsyncMethod()et les exceptions levées MyAsyncMethod()sont traitées dans OnMyAsyncMethodFailed(Task task)et non dans le try/ catcharoundGetStringData()


9
Supprimez Console.ReadLine();et ajoutez un peu de sommeil / retard MyAsyncMethodet vous ne verrez jamais l'exception.
tymtam

17

Je me retrouve avec cette solution:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}


2

Je suppose que la question se pose, pourquoi auriez-vous besoin de faire cela? La raison de asyncC # 5.0 est que vous pouvez attendre un résultat. Cette méthode n'est pas réellement asynchrone, mais simplement appelée à la fois afin de ne pas trop interférer avec le thread courant.

Il serait peut-être préférable de démarrer un thread et de le laisser se terminer seul.


2
asyncest un peu plus que simplement "attendre" un résultat. "attendre" implique que les lignes qui suivent "attendent" sont exécutées de manière asynchrone sur le même thread qui a appelé "attendre". Cela peut être fait sans "attendre", bien sûr, mais vous finissez par avoir un tas de délégués et vous perdez l'apparence séquentielle du code (ainsi que la possibilité d'utiliser usinget try/catch...
Peter Ritchie

1
@PeterRitchie Vous pouvez avoir une méthode fonctionnellement asynchrone, n'utilise pas le awaitmot - clé et n'utilise pas le asyncmot - clé, mais il n'y a aucun intérêt à utiliser le asyncmot - clé sans utiliser également awaitdans la définition de cette méthode.
Servy

1
@PeterRitchie De la déclaration: " asyncest un peu plus que" l'attente "d'un résultat." Le asyncmot-clé (vous avez laissé entendre que c'est le mot-clé en le mettant entre guillemets) ne signifie rien de plus qu'attendre le résultat. C'est l'asynchronie, comme les concepts CS généraux, cela signifie plus que simplement attendre un résultat.
Servy

1
@Servy asynccrée une machine d'état qui gère toutes les attentes dans la méthode async. S'il n'y a pas de awaits dans la méthode, elle crée toujours cette machine d'état - mais la méthode n'est pas asynchrone. Et si la asyncméthode revient void, il n'y a rien à attendre. Donc, c'est plus qu'attendre un résultat.
Peter Ritchie

1
@PeterRitchie Tant que la méthode renvoie une tâche, vous pouvez l'attendre. Il n'y a pas besoin de la machine d'état ou du asyncmot - clé. Tout ce que vous devez faire (et tout ce qui se passe finalement avec une machine à états dans ce cas particulier) est que la méthode est exécutée de manière synchrone puis encapsulée dans une tâche terminée. Je suppose que techniquement, vous ne supprimez pas simplement la machine d'état; vous supprimez la machine d'état, puis appelez Task.FromResult. Je supposais que vous (et aussi les rédacteurs du compilateur) pourriez ajouter l'addendum par vous-même.
Servy

0

Sur les technologies avec des boucles de messages (vous ne savez pas si ASP en fait partie), vous pouvez bloquer la boucle et traiter les messages jusqu'à la fin de la tâche et utiliser ContinueWith pour débloquer le code:

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

Cette approche est similaire au blocage sur ShowDialog et à la réactivité de l'interface utilisateur.


0

Je suis en retard à la fête ici, mais il y a une bibliothèque géniale que j'utilise et que je n'ai pas vue référencée dans les autres réponses

https://github.com/brminnick/AsyncAwaitBestPractices

Si vous devez "Fire And Forget" vous appelez la méthode d'extension sur la tâche.

Passer l'action onException à l'appel garantit que vous obtenez le meilleur des deux mondes - pas besoin d'attendre l'exécution et de ralentir vos utilisateurs, tout en conservant la possibilité de gérer l'exception de manière gracieuse.

Dans votre exemple, vous l'utiliseriez comme ceci:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

Il fournit également des AsyncCommands attendus implémentant ICommand, ce qui est idéal pour ma solution MVVM Xamarin


-1

La solution consiste à démarrer le HttpClient dans une autre tâche d'exécution sans contexte de sincronisation:

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);

-1

Si vous voulez vraiment faire ça. Juste pour adresser "Appeler une méthode asynchrone en C # sans attendre", vous pouvez exécuter la méthode asynchrone à l'intérieur d'un Task.Run. Cette approche attendra la MyAsyncMethodfin.

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

awaitdéballe de manière asynchrone la Resulttâche, tandis que l'utilisation de Résultat bloquerait jusqu'à ce que la tâche soit terminée.


-1

La méthode asynchrone renvoie généralement la classe Task. Si vous utilisez une Wait()méthode ou une Resultpropriété et que le code lève une exception - le type d'exception est enveloppé dans AggregateException- vous devez alors interroger Exception.InnerExceptionpour localiser l'exception correcte.

Mais il est également possible d'utiliser à la .GetAwaiter().GetResult()place - il attendra également la tâche asynchrone, mais ne terminera pas l'exception.

Voici donc un petit exemple:

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

Vous voudrez peut-être également être en mesure de renvoyer certains paramètres de la fonction asynchrone - cela peut être réalisé en fournissant un supplément Action<return type>dans la fonction asynchrone, par exemple comme ceci:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

Veuillez noter que les méthodes asynchrones ont généralement un ASyncnom de suffixe, juste pour éviter la collision entre les fonctions de synchronisation avec le même nom. (Par exemple FileStream.ReadAsync) - J'ai mis à jour les noms de fonctions pour suivre cette recommandation.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.