async / wait - quand retourner une tâche vs un vide?

503

Dans quels scénarios voudrait-on utiliser

public async Task AsyncMethod(int num)

au lieu de

public async void AsyncMethod(int num)

Le seul scénario auquel je peux penser est si vous avez besoin de la tâche pour pouvoir suivre sa progression.

De plus, dans la méthode suivante, les mots clés asynchrones et en attente sont-ils inutiles?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}
user981225
la source
20
Notez que les méthodes asynchrones doivent toujours être suffixées avec le nom Async.Example Foo()deviendrait FooAsync().
Fred
30
@Fred Surtout, mais pas toujours. Ceci est juste la convention et les exceptions acceptées à cette convention concernent les classes basées sur les événements ou les contrats d'interface, voir MSDN . Par exemple, vous ne devez pas renommer les gestionnaires d'événements courants, tels que Button1_Click.
Ben
14
Juste une note que vous ne devriez pas utiliser Thread.Sleepavec vos tâches, vous devriez await Task.Delay(num)plutôt
Bob Vale
45
@fred Je ne suis pas d'accord avec cela, l'OMI ajoutant un suffixe asynchrone ne doit être utilisé que lorsque vous fournissez une interface avec des options de synchronisation et d'async. Schtroumpf nommer des choses avec async quand il n'y a qu'une seule intention est inutile. Le cas d'espèce Task.Delayn'est pas le cas, Task.AsyncDelaycar toutes les méthodes de la tâche sont Async
Pas aimé
11
J'ai eu un problème intéressant ce matin avec une méthode de contrôleur webapi 2, il a été déclaré à la async voidplace async Task. La méthode s'est bloquée car elle utilisait un objet de contexte Entity Framework déclaré en tant que membre du contrôleur a été supprimé avant la fin de l'exécution de la méthode. L'infrastructure a disposé le contrôleur avant que sa méthode ne soit exécutée. J'ai changé la méthode en tâche asynchrone et cela a fonctionné.
costa

Réponses:

417

1) Normalement, vous voudriez retourner a Task. La principale exception devrait être lorsque vous devez avoir un voidtype de retour (pour les événements). S'il n'y a aucune raison de ne pas autoriser l'appelant à effectuer awaitvotre tâche, pourquoi la refuser?

2) les asyncméthodes qui retournent voidsont spéciales sous un autre aspect: elles représentent des opérations asynchrones de niveau supérieur et ont des règles supplémentaires qui entrent en jeu lorsque votre tâche renvoie une exception. La façon la plus simple est de montrer la différence avec un exemple:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fL'exception est toujours "observée". Une exception qui laisse une méthode asynchrone de niveau supérieur est simplement traitée comme toute autre exception non gérée. gL'exception n'est jamais observée. Lorsque le garbage collector vient nettoyer la tâche, il voit que la tâche a généré une exception et que personne n'a géré l'exception. Lorsque cela se produit, le TaskScheduler.UnobservedTaskExceptiongestionnaire s'exécute. Vous ne devriez jamais laisser cela se produire. Pour utiliser votre exemple,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Oui, utilisez asyncet awaitici, ils s'assurent que votre méthode fonctionne toujours correctement si une exception est levée.

pour plus d'informations, voir: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx

suizo
la source
10
Je voulais dire fau lieu de gdans mon commentaire. L'exception de fest passée à SynchronizationContext. gva augmenter UnobservedTaskException, mais UTEne plante plus le processus s'il n'est pas géré. Il existe des situations où il est acceptable d'avoir des "exceptions asynchrones" comme celle-ci qui sont ignorées.
Stephen Cleary
3
Si vous en avez WhenAnyplusieurs Taskentraînant des exceptions. Vous n'avez souvent qu'à gérer le premier, et vous voulez souvent ignorer les autres.
Stephen Cleary
1
@StephenCleary Merci, je suppose que c'est un bon exemple, même si cela dépend de la raison pour laquelle vous appelez WhenAnyen premier lieu s'il est correct d'ignorer les autres exceptions: le principal cas d'utilisation que j'ai pour lui finit toujours par attendre les tâches restantes à la fin de toute tâche , avec ou sans exception.
10
Je suis un peu confus quant à la raison pour laquelle vous recommandez de retourner une tâche au lieu de l'annuler. Comme vous l'avez dit, f () lancera et exception, mais g () ne le fera pas. N'est-il pas préférable d'être informé de ces exceptions de thread d'arrière-plan?
user981225
2
@ user981225 En effet, mais cela devient alors la responsabilité de l'appelant de g: chaque méthode qui appelle g doit être asynchrone et utiliser attendre aussi. Ceci est une directive, pas une règle stricte, vous pouvez décider que dans votre programme spécifique, il est plus facile d'avoir g return void.
40

J'ai rencontré cet article très utile sur asyncet voidécrit par Jérôme Laban: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

L'essentiel est qu'un an async+voidpeut planter le système et ne doit généralement être utilisé que sur les gestionnaires d'événements secondaires de l'interface utilisateur.

La raison derrière cela est le contexte de synchronisation utilisé par AsyncVoidMethodBuilder, étant nul dans cet exemple. Lorsqu'il n'y a pas de contexte de synchronisation ambiant, toute exception non gérée par le corps d'une méthode async void est renvoyée sur le ThreadPool. Bien qu'il n'y ait apparemment aucun autre endroit logique où ce type d'exception non gérée pourrait être levée, l'effet malheureux est que le processus est en train de se terminer, car les exceptions non gérées sur le ThreadPool terminent effectivement le processus depuis .NET 2.0. Vous pouvez intercepter toutes les exceptions non gérées à l'aide de l'événement AppDomain.UnhandledException, mais il n'existe aucun moyen de récupérer le processus à partir de cet événement.

Lors de l'écriture de gestionnaires d'événements d'interface utilisateur, les méthodes void asynchrones sont en quelque sorte indolores car les exceptions sont traitées de la même manière que dans les méthodes non asynchrones; ils sont lancés sur le Dispatcher. Il est possible de se remettre de telles exceptions, ce qui est plus que correct dans la plupart des cas. Cependant, en dehors des gestionnaires d'événements de l'interface utilisateur, les méthodes async void sont dangereuses à utiliser et peuvent être difficiles à trouver.

Davide Icardi
la source
Est-ce exact? Je pensais que les exceptions à l'intérieur des méthodes asynchrones étaient capturées dans Task. Et aussi afaik il y a toujours un défaut de synchronisation SynchronizationContext
NM
30

J'ai eu une idée claire de ces déclarations.

  1. Les méthodes async void ont différentes sémantiques de gestion des erreurs. Lorsqu'une exception est levée d'une tâche asynchrone ou d'une méthode de tâche asynchrone, cette exception est capturée et placée sur l'objet Task. Avec les méthodes async void, il n'y a pas d'objet Task, donc toutes les exceptions levées d'une méthode async void seront levées directement sur le SynchronizationContext (SynchronizationContext représente un emplacement "où" le code pourrait être exécuté.) Qui était actif lorsque la méthode async void commencé

Les exceptions d'une méthode Async Void ne peuvent pas être détectées avec Catch

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Ces exceptions peuvent être observées à l'aide d'AppDomain.UnhandledException ou d'un événement fourre-tout similaire pour les applications GUI / ASP.NET, mais l'utilisation de ces événements pour la gestion régulière des exceptions est une recette pour la non-maintenance (elle bloque l'application).

  1. Les méthodes async void ont une sémantique de composition différente. Les méthodes asynchrones renvoyant Task ou Task peuvent être facilement composées à l'aide de wait, Task.WhenAny, Task.WhenAll et ainsi de suite. Les méthodes asynchrones renvoyant void ne permettent pas de notifier facilement le code appelant qu'elles ont terminé. Il est facile de démarrer plusieurs méthodes void asynchrones, mais il n'est pas facile de déterminer quand elles ont fini. Les méthodes async void notifieront leur SynchronizationContext au démarrage et à la fin, mais un SynchronizationContext personnalisé est une solution complexe pour le code d'application normal.

  2. La méthode Async Void est utile lors de l'utilisation d'un gestionnaire d'événements synchrone car elle déclenche ses exceptions directement sur le SynchronizationContext, ce qui est similaire à la façon dont les gestionnaires d'événements synchrones se comportent

Pour plus de détails, consultez ce lien https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Nayas Subramanian
la source
21

Le problème avec l'appel async void est que vous ne récupérez même pas la tâche, vous n'avez aucun moyen de savoir quand la tâche de la fonction est terminée (voir https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

Voici les trois façons d'appeler une fonction asynchrone:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

Dans tous les cas, la fonction se transforme en une chaîne de tâches. La différence est ce que renvoie la fonction.

Dans le premier cas, la fonction renvoie une tâche qui produit éventuellement le t.

Dans le second cas, la fonction retourne une tâche qui n'a pas de produit, mais vous pouvez toujours l'attendre pour savoir quand elle s'est terminée.

Le troisième cas est le méchant. Le troisième cas est comme le deuxième cas, sauf que vous ne récupérez même pas la tâche. Vous n'avez aucun moyen de savoir quand la tâche de la fonction est terminée.

Le cas async void est un "feu et oubliez": vous démarrez la chaîne de tâches, mais vous ne vous souciez pas quand il est terminé. Lorsque la fonction revient, tout ce que vous savez, c'est que tout jusqu'au premier attente s'est exécuté. Après la première attente, tout se déroulera à un moment non spécifié auquel vous n'avez pas accès.

user8128167
la source
6

Je pense que vous pouvez également l'utiliser async voidpour lancer des opérations en arrière-plan, tant que vous prenez soin de détecter les exceptions. Pensées?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}
bboyle1234
la source
1
Le problème avec l'appel async void est que vous ne récupérez même pas la tâche, vous n'avez aucun moyen de savoir quand la tâche de la fonction est terminée
user8128167
Cela a du sens pour les opérations d'arrière-plan (rares) où vous ne vous souciez pas du résultat et vous ne voulez pas qu'une exception affecte d'autres opérations. Un cas d'utilisation typique serait d'envoyer un événement de journal à un serveur de journal. Vous voulez que cela se produise en arrière-plan et vous ne voulez pas que votre gestionnaire de service / demande échoue si le serveur de journalisation est en panne. Bien sûr, vous devez intercepter toutes les exceptions, sinon votre processus sera interrompu. Ou existe-t-il un meilleur moyen d'y parvenir?
Florian Winter
Les exceptions d'une méthode Async Void ne peuvent pas être détectées avec Catch. msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi
3
@SiZi la capture est DANS la méthode async void comme indiqué dans l'exemple et elle est interceptée.
bboyle1234
Qu'en est-il lorsque vos exigences changent pour exiger de ne pas travailler si vous ne pouvez pas le journaliser - alors vous devez modifier les signatures dans l'ensemble de votre API, au lieu de simplement changer la logique qui a actuellement // Ignorer l'exception
Milney
0

Ma réponse est simple, vous ne pouvez pas attendre la méthode nulle

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

Donc, si la méthode est asynchrone, il vaut mieux être attendu, car vous pouvez perdre l'avantage asynchrone.

Serg Shevchenko
la source
-2

Selon la documentation de Microsoft , ne doit JAMAIS utiliserasync void

Ne faites pas cela: l'exemple suivant utilise async voidce qui rend la requête HTTP terminée lorsque la première attente est atteinte:

  • Ce qui est TOUJOURS une mauvaise pratique dans les applications ASP.NET Core.

  • Accède à HttpResponse une fois la requête HTTP terminée.

  • Arrête le processus.

Robouste
la source