web-dev-qa-db-fra.com

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

J'ai 3 tâches:

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

Ils doivent tous être exécutés 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

Comment puis-je appeler et attendre que les 3 tâches soient terminées et ensuite obtenir les résultats?

191
BahaiResearch.com

Après avoir utilisé 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 maintenant que tout s’est terminé avec succès). Cependant, je recommande d'utiliser await car il est clairement correct, alors que Result peut entraîner des problèmes dans d'autres scénarios.

320
Stephen Cleary

Juste await les 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;
74
Servy

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)
    {
        await Task.WhenAll(task1, task2);
        return (task1.Result, task2.Result);
    }
}

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

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

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

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;
11
Reed Copsey

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

Disons que nous avons, 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 ... cela ne convient pas au traitement des résultats; nous voudrions typiquement await que:

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 entraîne beaucoup de surcharge et alloue divers tableaux (y compris le tableau params Task[]) et des listes (en interne). Cela fonctionne, mais ce n'est pas génial, OMI. À bien des égards, il est plus simple d'utiliser une opération async et juste await chacune à 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, utiliser await au lieu de Task.WhenAll fait pas de différence comment les tâches sont exécutées (simultanément, séquentiellement, etc.). Au plus haut niveau, Task.WhenAllprédates bon support du compilateur pour async/await, et était utile lorsque ces choses ne n'existe pas . C'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/await génère beaucoup de bruit de compilation pour la suite. S'il est probable que les tâches pourraient se terminer de manière synchrone, nous pouvons l'optimiser en construisant un chemin synchrone avec un repli 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 "chemin de synchronisation avec repli asynchrone" est de plus en plus courante, en particulier dans le code haute performance où les complétions 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 un C # récent, un modèle commun à la méthode de repliement async est généralement implémenté 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érez ValueTask<T> à Task<T> s'il y a de fortes chances que les choses soient toujours parfaitement synchrones avec de nombreuses valeurs de retour différentes:

    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érez IsCompletedSuccessfully à Status == TaskStatus.RanToCompletion; cela existe maintenant dans .NET Core pour Task, et partout pour ValueTask<T>

10
Marc Gravell

Si 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 sur carTask. Il y a 3 scénarios possibles ici:

  1. SellHouse est déjà terminé avec succès en cas d’échec de FeedCat. Dans ce cas, ça va.

  2. SellHouse n'est pas complet et échoue exceptionnellement à un moment donné. Les exceptions ne sont pas observées et seront réexécutées sur le thread du finaliseur.

  3. SellHouse n'est pas complet et contient des attentes à l'intérieur. Si votre code s'exécute dans ASP.NET, SellHouse échouera dès qu'une partie de l'attente sera terminée. Cela est dû au fait que vous avez essentiellement déclenché un appel et que le contexte de synchronisation a été perdu dès que FeedCat a échoué.

Voici une 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()<---

Dans le cas (2), vous obtiendrez une erreur similaire, mais avec la trace de pile d’exception originale.

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

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

3
samfromlv

Vous pouvez utiliser Task.WhenAll comme indiqué ou Task.WaitAll, selon que vous voulez que le thread attende ou non. Regardez le lien pour une explication des deux.

WaitAll vs WhenAll

2
christiandev

Utilisez Task.WhenAll et 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.
2
It'sNotALie.
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.

1
taurius

Avertissement en avant

Juste une petite tête à ceux qui visitent ce sujet et d’autres similaires qui cherchent un moyen de paralléliser EntityFramework en utilisant async + wait + tool tool set: Le motif présenté ici est correct, cependant, en ce qui concerne le flocon de neige spécial de EF, vous ne pourrez pas exécuter l’exécution en parallèle à moins que et jusqu’à ce que vous n’utilisiez une instance (nouvelle) db-context-instance distincte à l’intérieur de chaque appel * Async () impliqué.

Ce genre de chose est nécessaire en raison des limitations inhérentes à la conception des contextes ef-db qui interdisent l'exécution de 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 donnent lieu à 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 autre implémentation présentant 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);
     }
 }
0
XDS