Comment utiliser Wait dans une boucle

86

J'essaie de créer une application console asynchrone qui travaille sur une collection. J'ai une version qui utilise la boucle parallèle pour une autre version qui utilise async / await. Je m'attendais à ce que la version async / await fonctionne de manière similaire à la version parallèle, mais elle s'exécute de manière synchrone. Qu'est-ce que je fais mal?

class Program
{
    static void Main(string[] args)
    {
        var worker = new Worker();
        worker.ParallelInit();
        var t = worker.Init();
        t.Wait();
        Console.ReadKey();
    }
}

public class Worker
{
    public async Task<bool> Init()
    {
        var series = Enumerable.Range(1, 5).ToList();
        foreach (var i in series)
        {
            Console.WriteLine("Starting Process {0}", i);
            var result = await DoWorkAsync(i);
            if (result)
            {
                Console.WriteLine("Ending Process {0}", i);
            }
        }

        return true;
    }

    public async Task<bool> DoWorkAsync(int i)
    {
        Console.WriteLine("working..{0}", i);
        await Task.Delay(1000);
        return true;
    }

    public bool ParallelInit()
    {
        var series = Enumerable.Range(1, 5).ToList();
        Parallel.ForEach(series, i =>
        {
            Console.WriteLine("Starting Process {0}", i);
            DoWorkAsync(i);
            Console.WriteLine("Ending Process {0}", i);
        });
        return true;
    }
}
Satish
la source

Réponses:

124

La façon dont vous utilisez le awaitmot - clé indique à C # que vous voulez attendre chaque fois que vous passez par la boucle, ce qui n'est pas parallèle. Vous pouvez réécrire votre méthode comme ceci pour faire ce que vous voulez, en stockant une liste de Tasks et awaiten les intégrant tous avec Task.WhenAll.

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<Tuple<int, bool>>>();
    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }
    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.Item2)
        {
            Console.WriteLine("Ending Process {0}", task.Item1);
        }
    }
    return true;
}

public async Task<Tuple<int, bool>> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return Tuple.Create(i, true);
}
Tim S.
la source
3
Je ne sais pas pour les autres, mais un parallèle for / foreach semble plus simple pour les boucles parallèles.
Brettski
8
Il est important de noter que lorsque vous voyez la Ending Processnotification, ce n'est pas lorsque la tâche se termine réellement. Toutes ces notifications sont vidées de manière séquentielle juste après la fin de la dernière tâche. Au moment où vous voyez "Fin du processus 1", le processus 1 est peut-être terminé depuis longtemps. Autre que le choix des mots, +1.
Asad Saeeduddin
@Brettski Je me trompe peut-être, mais une boucle parallèle intercepte tout type de résultat asynchrone. En renvoyant un Task <T> vous récupérez immédiatement un objet Task dans lequel vous pouvez gérer le travail qui se passe à l'intérieur, comme l'annuler ou voir des exceptions. Désormais, avec Async / Await, vous pouvez travailler avec l'objet Task de manière plus conviviale, c'est-à-dire que vous n'avez pas à faire Task.Result.
The Muffin Man
@Tim S, que faire si je souhaite renvoyer une valeur avec la fonction asynchrone à l'aide de la méthode Tasks.WhenAll?
Mihir
Serait-ce une mauvaise pratique d'implémenter un Semaphorein DoWorkAsyncpour limiter le maximum de tâches d'exécution?
C4d
39

Votre code attend la fin de chaque opération (utilisation await) avant de commencer l'itération suivante.
Par conséquent, vous n'obtenez aucun parallélisme.

Si vous souhaitez exécuter une opération asynchrone existante en parallèle, vous n'avez pas besoin await; il vous suffit d'obtenir une collection de Tasks et d'appeler Task.WhenAll()pour renvoyer une tâche qui les attend tous:

return Task.WhenAll(list.Select(DoWorkAsync));
SLaks
la source
vous ne pouvez donc pas utiliser de méthodes asynchrones dans aucune boucle?
Satish
4
@Satish: Vous pouvez. Cependant, awaitfait exactement le contraire de ce que vous voulez - il attend la Taskfin.
SLaks
Je voulais accepter votre réponse mais Tims S a une meilleure réponse.
Satish
Ou si vous n'avez pas besoin de savoir quand la tâche est terminée, vous pouvez simplement appeler les méthodes sans les attendre
disklosr
Pour confirmer ce que fait cette syntaxe - elle exécute la tâche appelée DoWorkAsyncsur chaque élément list(en passant chaque élément dans DoWorkAsync, qui, je suppose, a un seul paramètre)?
jbyrd
12
public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5);
    Task.WhenAll(series.Select(i => DoWorkAsync(i)));
    return true;
}
Vladimir
la source
4

En C # 7.0, vous pouvez utiliser des noms sémantiques pour chacun des membres du tuple , voici la réponse de Tim S. en utilisant la nouvelle syntaxe:

public async Task<bool> Init()
{
    var series = Enumerable.Range(1, 5).ToList();
    var tasks = new List<Task<(int Index, bool IsDone)>>();

    foreach (var i in series)
    {
        Console.WriteLine("Starting Process {0}", i);
        tasks.Add(DoWorkAsync(i));
    }

    foreach (var task in await Task.WhenAll(tasks))
    {
        if (task.IsDone)
        {
            Console.WriteLine("Ending Process {0}", task.Index);
        }
    }

    return true;
}

public async Task<(int Index, bool IsDone)> DoWorkAsync(int i)
{
    Console.WriteLine("working..{0}", i);
    await Task.Delay(1000);
    return (i, true);
}

Vous pouvez également vous débarrasser de l' task. intérieurforeach :

// ...
foreach (var (IsDone, Index) in await Task.WhenAll(tasks))
{
    if (IsDone)
    {
        Console.WriteLine("Ending Process {0}", Index);
    }
}
// ...
Mehdi Dehghani
la source