En attente de plusieurs tâches avec des résultats différents

237

J'ai 3 tâches:

private async Task<Cat> FeedCat() {}
private async Task<House> SellHouse() {}
private async Task<Tesla> BuyCar() {}

Ils doivent tous s'exécuter avant que mon code puisse continuer et j'ai également besoin des résultats de chacun. Aucun des résultats n'a rien en commun les uns avec les autres

Comment puis-je appeler et attendre la fin des 3 tâches, puis obtenir les résultats?

Ian Vink
la source
25
Avez-vous des exigences de commande? Autrement dit, voulez-vous ne vendre la maison qu'après avoir nourri le chat?
Eric Lippert

Réponses:

412

Après utilisation WhenAll, vous pouvez extraire les résultats individuellement avec await:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Vous pouvez également utiliser Task.Result(puisque vous savez à ce stade, ils ont tous réussi). Cependant, je recommande d'utiliser awaitcar c'est clairement correct, alors que cela Resultpeut causer des problèmes dans d'autres scénarios.

Stephen Cleary
la source
83
Vous pouvez simplement supprimer WhenAllcomplètement le de ceci; les attentes veilleront à ce que vous ne dépassiez pas les 3 affectations ultérieures tant que les tâches ne sont pas terminées.
Servy
134
Task.WhenAll()permet d'exécuter la tâche en mode parallèle . Je ne comprends pas pourquoi @Servy a suggéré de le supprimer. Sans cela, WhenAllils seront exécutés un par un
Sergey G.
87
@Sergey: les tâches commencent à s'exécuter immédiatement. Par exemple, catTaskest déjà en cours d'exécution au moment de son retour FeedCat. Donc, l'une ou l'autre approche fonctionnera - la seule question est de savoir si vous voulez les utiliser awaitune à la fois ou toutes ensemble. La gestion des erreurs est légèrement différente - si vous utilisez Task.WhenAll, alors tout le monde le fera await, même si l'un d'eux échoue tôt.
Stephen Cleary
23
@Sergey Calling WhenAlln'a aucun impact sur le moment où les opérations s'exécutent, ni sur la façon dont elles s'exécutent. Il ne dispose d' aucune possibilité d'effectuer la façon dont les résultats sont observés. Dans ce cas particulier, la seule différence est qu'une erreur dans l'une des deux premières méthodes entraînerait la levée de l'exception dans cette pile d'appels plus tôt dans ma méthode que celle de Stephen (bien que la même erreur soit toujours levée, s'il y en a) ).
Servy
37
@Sergey: La clé est que les méthodes asynchrones renvoient toujours des tâches "chaudes" (déjà démarrées).
Stephen Cleary
99

Juste awaitles trois tâches séparément, après les avoir toutes démarrées.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;
Servy
la source
8
@Bargitta Non, c'est faux. Ils feront leur travail en parallèle. N'hésitez pas à l'exécuter et à voir par vous-même.
Servy
5
Les gens continuent de poser la même question après des années ... Je pense qu'il est important de souligner à nouveau qu'une tâche " commence à créer " dans le corps de la réponse : peut-être qu'ils ne prennent pas la peine de lire les commentaires
9
@StephenYork L'ajout ne Task.WhenAllchange littéralement rien au comportement du programme, de quelque manière que ce soit. Il s'agit d'un appel de méthode purement redondant. Vous pouvez l'ajouter, si vous le souhaitez, comme choix esthétique, mais cela ne change pas ce que fait le code. Le temps d'exécution du code sera identique avec ou sans cet appel de méthode (eh bien, techniquement, il y aura une très petite surcharge pour appeler WhenAll, mais cela devrait être négligeable), ce qui rend cette version légèrement plus longue à exécuter que cette version.
Servy
4
@StephenYork Votre exemple exécute les opérations séquentiellement pour deux raisons. Vos méthodes asynchrones ne sont pas réellement asynchrones, elles sont synchrones. Le fait que vous ayez des méthodes synchrones qui renvoient toujours des tâches déjà terminées les empêche de s'exécuter simultanément. Ensuite, vous ne faites pas réellement ce qui est indiqué dans cette réponse: démarrer les trois méthodes asynchrones, puis attendre les trois tâches à tour de rôle. Votre exemple n'appelle pas chaque méthode jusqu'à la fin de la précédente, ce qui empêche explicitement le démarrage d'une jusqu'à la fin de la précédente, contrairement à ce code.
Servy
4
@MarcvanNieuwenhuijzen Ce n'est manifestement pas vrai, comme cela a été discuté dans les commentaires ici, et sur d'autres réponses. L'ajout WhenAllest un changement purement esthétique. La seule différence de comportement observable est de savoir si vous attendez que les tâches ultérieures se terminent en cas de défaillance d'une tâche antérieure, ce qui n'est généralement pas nécessaire. Si vous ne croyez pas aux nombreuses explications pour lesquelles votre déclaration n'est pas vraie, vous pouvez simplement exécuter le code par vous-même et voir que ce n'est pas vrai.
Servy
37

Si vous utilisez C # 7, vous pouvez utiliser une méthode d'emballage pratique comme celle-ci ...

public static class TaskEx
{
    public static async Task<(T1, T2)> WhenAll<T1, T2>(Task<T1> task1, Task<T2> task2)
    {
        return (await task1, await task2);
    }
}

... pour activer une syntaxe pratique comme celle-ci lorsque vous souhaitez attendre plusieurs tâches avec différents types de retour. Il faudrait bien sûr faire plusieurs surcharges pour différents nombres de tâches à attendre.

var (someInt, someString) = await TaskEx.WhenAll(GetIntAsync(), GetStringAsync());

Cependant, consultez la réponse de Marc Gravell pour quelques optimisations autour de ValueTask et des tâches déjà terminées si vous avez l'intention de transformer cet exemple en quelque chose de réel.

Joel Mueller
la source
Les tuples sont la seule fonctionnalité C # 7 impliquée ici. Ce sont définitivement dans la version finale.
Joel Mueller
Je connais les tuples et c # 7. Je veux dire que je ne trouve pas la méthode WhenAll qui retourne les tuples. Quel espace de noms / package?
Yury Scherbakov
@YuryShcherbakov Task.WhenAll()ne retourne pas de tuple. L'une est en cours de construction à partir des Resultpropriétés des tâches fournies une fois la tâche renvoyée par Task.WhenAll()terminée.
Chris Charabaruk
2
Je suggère de remplacer les .Resultappels selon le raisonnement de Stephen pour éviter que d'autres personnes ne perpétuent la mauvaise pratique en copiant votre exemple.
julealgon
Je me demande pourquoi cette méthode ne fait pas partie de ce cadre? Cela semble tellement utile. Ont-ils manqué de temps et ont dû s'arrêter à un seul type de retour?
Ian Grainger
14

Étant donné trois tâches - FeedCat(), SellHouse()et BuyCar(), il y a deux cas intéressants: soit ils se terminent tous de manière synchrone (pour une raison quelconque, peut-être la mise en cache ou une erreur), soit ils ne le font pas.

Disons que nous avons, à partir de la question:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();
    // what here?
}

Maintenant, une approche simple serait:

Task.WhenAll(x, y, z);

mais ... ce n'est pas pratique pour traiter les résultats; nous voudrions généralement await:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    await Task.WhenAll(x, y, z);
    // presumably we want to do something with the results...
    return DoWhatever(x.Result, y.Result, z.Result);
}

mais cela fait beaucoup de surcharge et alloue divers tableaux (y compris le params Task[]tableau) et listes (en interne). Cela fonctionne, mais ce n'est pas génial IMO. À bien des égards, il est plus simple d'utiliser une asyncopération et awaitchacune à son tour:

async Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    // do something with the results...
    return DoWhatever(await x, await y, await z);
}

Contrairement à certains des commentaires ci-dessus, l'utilisation awaitau lieu de neTask.WhenAll fait aucune différence sur la façon dont les tâches s'exécutent (simultanément, séquentiellement, etc.). Au plus haut niveau, il Task.WhenAll est antérieur au bon support du compilateur pour async/ awaitet était utile lorsque ces choses n'existaient pas . Il est également utile lorsque vous avez un tableau arbitraire de tâches, plutôt que 3 tâches discrètes.

Mais: nous avons toujours le problème que async/ awaitgénère beaucoup de bruit de compilation pour la suite. S'il est probable que les tâches puissent effectivement se terminer de manière synchrone, nous pouvons optimiser cela en créant un chemin synchrone avec une solution de secours asynchrone:

Task<string> DoTheThings() {
    Task<Cat> x = FeedCat();
    Task<House> y = SellHouse();
    Task<Tesla> z = BuyCar();

    if(x.Status == TaskStatus.RanToCompletion &&
       y.Status == TaskStatus.RanToCompletion &&
       z.Status == TaskStatus.RanToCompletion)
        return Task.FromResult(
          DoWhatever(a.Result, b.Result, c.Result));
       // we can safely access .Result, as they are known
       // to be ran-to-completion

    return Awaited(x, y, z);
}

async Task Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
    return DoWhatever(await x, await y, await z);
}

Cette approche de "chemin de synchronisation avec repli asynchrone" est de plus en plus courante, en particulier dans le code haute performance où les exécutions synchrones sont relativement fréquentes. Notez que cela n'aidera pas du tout si l'achèvement est toujours véritablement asynchrone.

Autres choses qui s'appliquent ici:

  1. avec C # récent, un modèle commun est que la asyncméthode de secours est généralement implémentée en tant que fonction locale:

    Task<string> DoTheThings() {
        async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        Task<Cat> x = FeedCat();
        Task<House> y = SellHouse();
        Task<Tesla> z = BuyCar();
    
        if(x.Status == TaskStatus.RanToCompletion &&
           y.Status == TaskStatus.RanToCompletion &&
           z.Status == TaskStatus.RanToCompletion)
            return Task.FromResult(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  2. préfèrent ValueTask<T>à Task<T>s'il y a une bonne chance de choses jamais tout à fait synchrone avec beaucoup de différentes valeurs de retour:

    ValueTask<string> DoTheThings() {
        async ValueTask<string> Awaited(ValueTask<Cat> a, Task<House> b, Task<Tesla> c) {
            return DoWhatever(await a, await b, await c);
        }
        ValueTask<Cat> x = FeedCat();
        ValueTask<House> y = SellHouse();
        ValueTask<Tesla> z = BuyCar();
    
        if(x.IsCompletedSuccessfully &&
           y.IsCompletedSuccessfully &&
           z.IsCompletedSuccessfully)
            return new ValueTask<string>(
              DoWhatever(a.Result, b.Result, c.Result));
           // we can safely access .Result, as they are known
           // to be ran-to-completion
    
        return Awaited(x, y, z);
    }
  3. si possible, préférer IsCompletedSuccessfullyà Status == TaskStatus.RanToCompletion; cela existe maintenant dans .NET Core pour Task, et partout pourValueTask<T>

Marc Gravell
la source
"Contrairement à diverses réponses ici, utiliser attendre au lieu de Task.WhenAll ne fait aucune différence sur la façon dont les tâches s'exécutent (simultanément, séquentiellement, etc.)" Je ne vois aucune réponse qui le dise. Je les aurais déjà commentés en disant autant s'ils l'avaient fait. Il y a beaucoup de commentaires sur beaucoup de réponses disant cela, mais pas de réponses. De quoi parlez-vous? Notez également que votre réponse ne gère pas le résultat des tâches (ou ne tient pas compte du fait que les résultats sont tous d'un type différent). Vous les avez composés dans une méthode qui renvoie simplement un Taskquand ils sont tous terminés sans utiliser les résultats.
Servy
@Servy vous avez raison, c'était des commentaires; J'ajouterai un tweak pour montrer en utilisant les résultats
Marc Gravell
@Servy tweak ajouté
Marc Gravell
De plus, si vous envisagez de supprimer les tâches synchrones le plus tôt possible, vous pouvez également gérer toutes les tâches annulées ou défectueuses de manière synchrone, plutôt que celles qui ont été terminées avec succès. Si vous avez décidé que c'est une optimisation dont votre programme a besoin (ce qui sera rare, mais qui se produira), vous pourriez aussi bien aller jusqu'au bout.
Servy
@Servy qui est un sujet complexe - vous obtenez une sémantique d'exception différente des deux scénarios - en attente de déclenchement d'une exception se comporte différemment de l'accès à .Result pour déclencher l'exception. OMI à ce stade, nous devrions awaitobtenir la "meilleure" sémantique des exceptions, en supposant que les exceptions sont rares mais significatives
Marc Gravell
12

Vous pouvez les stocker dans des tâches, puis les attendre tous:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

Cat cat = await catTask;
House house = await houseTask;
Car car = await carTask;
Reed Copsey
la source
n'exécute pas var catTask = FeedCat()la fonction FeedCat()et ne stocke pas le résultat pour catTaskrendre le await Task.WhenAll()type de pièce inutile car la méthode a déjà été exécutée ??
Kraang Prime
1
@sanuel s'ils retournent la tâche <t>, alors non ... ils commencent l'ouverture asynchrone, mais n'attendez pas
Reed Copsey
Je ne pense pas que ce soit exact, veuillez consulter les discussions sous la réponse de @ StephenCleary ... voir également la réponse de Servy.
Rosdi Kasim
1
si j'ai besoin d'ajouter .ConfigrtueAwait (false). Est-ce que je l'ajouterais juste à Task.WhenAll ou à chaque serveur qui suit?
AstroSharp
@AstroSharp en général, c'est une bonne idée de l'ajouter à chacun d'eux (si le premier est terminé, il est effectivement ignoré), mais dans ce cas, il serait probablement correct de faire le premier - à moins qu'il n'y ait plus d'async des choses qui se passent plus tard.
Reed Copsey
6

Dans le cas où vous essayez de consigner toutes les erreurs, assurez-vous de conserver la ligne Task.WhenAll dans votre code, de nombreux commentaires suggèrent que vous pouvez le supprimer et attendre des tâches individuelles. Task.WhenAll est vraiment important pour la gestion des erreurs. Sans cette ligne, vous laissez potentiellement votre code ouvert pour les exceptions non observées.

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

await Task.WhenAll(catTask, houseTask, carTask);

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Imagine FeedCat lève une exception dans le code suivant:

var catTask = FeedCat();
var houseTask = SellHouse();
var carTask = BuyCar();

var cat = await catTask;
var house = await houseTask;
var car = await carTask;

Dans ce cas, vous n'attendrez jamais sur houseTask ni carTask. Il y a 3 scénarios possibles ici:

  1. SellHouse est déjà terminé avec succès lorsque FeedCat a échoué. Dans ce cas, tout va bien.

  2. SellHouse n'est pas terminée et échoue avec exception à un moment donné. Aucune exception n'est observée et sera renvoyée sur le thread du finaliseur.

  3. SellHouse n'est pas terminée et contient des attentes à l'intérieur. Dans le cas où votre code s'exécute dans ASP.NET SellHouse échouera dès que certaines des attentes seront terminées à l'intérieur. Cela se produit car vous avez essentiellement déclenché et oublié le contexte d'appel et de synchronisation a été perdu dès que FeedCat a échoué.

Voici l'erreur que vous obtiendrez pour le cas (3):

System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()
   --- End of inner exception stack trace ---
---> (Inner Exception #0) System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
   at System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
   at System.Web.HttpApplication.System.Web.Util.ISyncContext.Enter()
   at System.Web.Util.SynchronizationHelper.SafeWrapCallback(Action action)
   at System.Threading.Tasks.Task.Execute()<---

Pour le cas (2), vous obtiendrez une erreur similaire mais avec une trace de pile d'exceptions d'origine.

Pour .NET 4.0 et versions ultérieures, vous pouvez intercepter les exceptions non observées à l'aide de TaskScheduler.UnobservedTaskException. Pour .NET 4.5 et les exceptions non observées ultérieures sont avalées par défaut pour .NET 4.0, l'exception non observée plantera votre processus.

Plus de détails ici: Gestion des exceptions de tâches dans .NET 4.5

samfromlv
la source
2

Vous pouvez utiliser Task.WhenAllcomme mentionné, ou Task.WaitAll, selon que vous souhaitez que le thread attende. Jetez un œil au lien pour une explication des deux.

WaitAll vs WhenAll

christiandev
la source
2

Utilisez Task.WhenAllpuis attendez les résultats:

var tCat = FeedCat();
var tHouse = SellHouse();
var tCar = BuyCar();
await Task.WhenAll(tCat, tHouse, tCar);
Cat cat = await tCat;
House house = await tHouse;
Tesla car = await tCar; 
//as they have all definitely finished, you could also use Task.Value.
It'sNotALie.
la source
mm ... pas Task.Value (peut-être existait-il en 2013?), plutôt tCat.Result, tHouse.Result ou tCar.Result
Stephen York
1

Avertissement avant

Juste un bref avertissement à ceux qui visitent ce sujet et d'autres threads similaires à la recherche d'un moyen de paralléliser EntityFramework en utilisant async + attendre + ensemble d'outils de tâche : le modèle montré ici est bon, cependant, quand il s'agit du flocon de neige spécial d'EF, vous ne le ferez pas réaliser une exécution parallèle à moins que et jusqu'à ce que vous utilisiez une (nouvelle) instance de contexte db distincte à l'intérieur de chaque appel * Async () impliqué.

Ce genre de chose est nécessaire en raison des limitations de conception inhérentes aux contextes ef-db qui interdisent d'exécuter plusieurs requêtes en parallèle dans la même instance de contexte ef-db.


En capitalisant sur les réponses déjà données, c'est le moyen de s'assurer que vous collectez toutes les valeurs même dans le cas où une ou plusieurs des tâches entraînent une exception:

  public async Task<string> Foobar() {
    async Task<string> Awaited(Task<Cat> a, Task<House> b, Task<Tesla> c) {
        return DoSomething(await a, await b, await c);
    }

    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        if (carTask.Status == TaskStatus.RanToCompletion //triple
            && catTask.Status == TaskStatus.RanToCompletion //cache
            && houseTask.Status == TaskStatus.RanToCompletion) { //hits
            return Task.FromResult(DoSomething(catTask.Result, carTask.Result, houseTask.Result)); //fast-track
        }

        cat = await catTask;
        car = await carTask;
        house = await houseTask;
        //or Task.AwaitAll(carTask, catTask, houseTask);
        //or await Task.WhenAll(carTask, catTask, houseTask);
        //it depends on how you like exception handling better

        return Awaited(catTask, carTask, houseTask);
   }
 }

Une implémentation alternative qui a plus ou moins les mêmes caractéristiques de performance pourrait être:

 public async Task<string> Foobar() {
    using (var carTask = BuyCarAsync())
    using (var catTask = FeedCatAsync())
    using (var houseTask = SellHouseAsync())
    {
        cat = catTask.Status == TaskStatus.RanToCompletion ? catTask.Result : (await catTask);
        car = carTask.Status == TaskStatus.RanToCompletion ? carTask.Result : (await carTask);
        house = houseTask.Status == TaskStatus.RanToCompletion ? houseTask.Result : (await houseTask);

        return DoSomething(cat, car, house);
     }
 }
XDS
la source
-1
var dn = await Task.WhenAll<dynamic>(FeedCat(),SellHouse(),BuyCar());

si vous souhaitez accéder à Cat, procédez comme suit:

var ct = (Cat)dn[0];

C'est très simple à faire et très utile à utiliser, il n'est pas nécessaire de rechercher une solution complexe.

taurius
la source
1
Il y a juste un problème avec ça: dynamicc'est le diable. C'est pour l'interopérabilité COM délicate et autres, et ne doit pas être utilisé dans toutes les situations où il n'est pas absolument nécessaire. Surtout si vous vous souciez de la performance. Ou tapez sécurité. Ou refactoring. Ou débogage.
Joel Mueller