web-dev-qa-db-fra.com

Exécuter des tâches en parallèle

Ok, donc en gros, j'ai un tas de tâches (10) et je veux les commencer toutes en même temps et attendre qu'elles se terminent. Une fois terminé, je veux exécuter d'autres tâches. J'ai lu beaucoup de ressources à ce sujet mais je ne peux pas le faire correctement pour mon cas particulier ...

Voici ce que j'ai actuellement (le code a été simplifié):

public async Task RunTasks()
{
    var tasks = new List<Task>
    {
        new Task(async () => await DoWork()),
        //and so on with the other 9 similar tasks
    }


    Parallel.ForEach(tasks, task =>
    {
        task.Start();
    });

    Task.WhenAll(tasks).ContinueWith(done =>
    {
        //Run the other tasks
    });
}

//This function perform some I/O operations
public async Task DoWork()
{
    var results = await GetDataFromDatabaseAsync();
    foreach (var result in results)
    {
        await ReadFromNetwork(result.Url);
    }
}

Le problème est donc que lorsque j'attends que les tâches soient terminées avec l'appel WhenAll, cela me dit que toutes les tâches sont terminées, même si aucune d'entre elles n'est terminée. J'ai essayé d'ajouter Console.WriteLine dans ma foreach et, lorsque j'ai saisi la tâche de continuation, les données provenant de ma précédente Task ne cessent d'arriver et ne sont pas vraiment terminées.

Qu'est-ce que je fais mal ici?

21
Yann Thibodeau

Vous ne devriez presque jamais utiliser le constructeur Task directement. Dans votre cas, cette tâche ne déclenche que la tâche réelle que vous ne pouvez pas attendre.

Vous pouvez simplement appeler DoWork et récupérer une tâche, la stocker dans une liste et attendre que toutes les tâches soient terminées. Sens:

tasks.Add(DoWork());
// ...
await Task.WhenAll(tasks);

Toutefois, les méthodes asynchrones s'exécutent de manière synchrone jusqu'à ce que la première attente d'une tâche non terminée soit atteinte. Si vous craignez que cette partie prenne trop de temps, utilisez Task.Run pour la décharger sur un autre thread ThreadPool, puis stockez that task dans la liste:

tasks.Add(Task.Run(() => DoWork()));
// ...
await Task.WhenAll(tasks);
23
i3arnon

Essentiellement, vous mélangez deux paradigmes asynchrones incompatibles. c'est-à-dire Parallel.ForEach() et async-await.

Pour ce que vous voulez, faites l’un ou l’autre. Par exemple. vous pouvez simplement utiliser Parallel.For[Each]() et abandonner complètement l'attente asynchrone. Parallel.For[Each]() ne reviendra que lorsque toutes les tâches parallèles seront terminées et vous pourrez alors passer aux autres tâches.

Le code a aussi d'autres problèmes:

  • vous marquez la méthode comme asynchrone mais n'attendez pas dedans (l'attente que vous avez est dans le délégué, pas dans la méthode);

  • vous souhaiterez presque certainement .ConfigureAwait(false) dans l'attente, surtout si vous n'essayez pas d'utiliser les résultats immédiatement dans un thread d'interface utilisateur.

8
sellotape

Si vous souhaitez exécuter ces tâches parallèlement dans différents threads à l'aide de TPL, vous aurez peut-être besoin de quelque chose comme ceci:

public async Task RunTasks()
{
    var tasks = new List<Func<Task>>
    {
       DoWork,
       //...
    };

    await Task.WhenAll(tasks.AsParallel().Select(async task => await task()));

    //Run the other tasks
}

Celles-ci ne mettent en parallèle que de petites quantités de code: la mise en file d'attente de la méthode sur le pool de threads et le retour d'une Task non complétée. De plus, pour une si petite quantité de tâches, la parallélisation peut prendre plus de temps que de simplement s'exécuter de manière asynchrone. Cela n’a de sens que si vos tâches font un travail plus long (synchrone) avant leur première attente.

Dans la plupart des cas, le meilleur moyen sera:

public async Task RunTasks()
{
    await Task.WhenAll(new [] 
    {
        DoWork(),
        //...
    });
    //Run the other tasks
}

A mon avis dans votre code:

  1. Vous ne devez pas envelopper votre code dans Task avant de passer à Parallel.ForEach.

  2. Vous pouvez simplement awaitTask.WhenAll au lieu d’utiliser ContinueWith.

2
Andrey Tretyak

La méthode DoWork est une méthode d'E/S asynchrone. Cela signifie que vous n'avez pas besoin de plusieurs threads pour en exécuter plusieurs, car la plupart du temps, la méthode attend de manière asynchrone la fin de l'entrée/sortie. Un fil suffit pour le faire.

public async Task RunTasks()
{
    var tasks = new List<Task>
    {
        DoWork(),
        //and so on with the other 9 similar tasks
    };

    await Task.WhenAll(tasks);

    //Run the other tasks            
}

Vous devriez presque ne jamais utiliser le constructeur Task pour créer une nouvelle tâche. Pour créer une tâche d'E/S asynchrone, appelez simplement la méthode async. Pour créer une tâche qui sera exécutée sur un thread de pool de threads, utilisez Task.Run. Vous pouvez lire cet article pour une explication détaillée de Task.Run et d’autres options de création de tâches.

1
Jakub Lortz

Juste aussi ajouter un bloc try-catch autour de la tâche.WhenAll

NB: Une instance de System.AggregateException est levée et agit comme un wrapper autour d'une ou plusieurs exceptions survenues. Ceci est important pour les méthodes qui coordonnent plusieurs tâches telles que Task.WaitAll () et Task.WaitAny () afin que AggregateException puisse encapsuler toutes les exceptions des tâches en cours.

try
{ 
    Task.WaitAll(tasks.ToArray());  
}
catch(AggregateException ex)
{ 
    foreach (Exception inner in ex.InnerExceptions)
    {
    Console.WriteLine(String.Format("Exception type {0} from {1}", inner.GetType(), inner.Source));
    }
}
0
NiTRiX-Reloaded