web-dev-qa-db-fra.com

Parallel.ForEach et async-wait

J'avais une telle méthode:

public async Task<MyResult> GetResult()
{
    MyResult result = new MyResult();

    foreach(var method in Methods)
    {
        string json = await Process(method);

        result.Prop1 = PopulateProp1(json);
        result.Prop2 = PopulateProp2(json);

    }

    return result;
}

J'ai alors décidé d'utiliser Parallel.ForEach:

public async Task<MyResult> GetResult()
{
    MyResult result = new MyResult();

    Parallel.ForEach(Methods, async method =>
    {
        string json = await Process(method);    

        result.Prop1 = PopulateProp1(json);
        result.Prop2 = PopulateProp2(json);
    });

    return result;
}

Mais maintenant j'ai une erreur:

Un module ou gestionnaire asynchrone s'est terminé alors qu'une opération asynchrone était toujours en attente.

37
sreginogemoh

async ne fonctionne pas bien avec ForEach. En particulier, votre async lambda est en cours de conversion en async void méthode. Il existe un certain nombre de raisons d'éviter async void (comme je le décris dans un article MSDN); l'un d'eux est que vous ne pouvez pas facilement détecter quand le async lambda est terminé. ASP.NET verra le retour de votre code sans avoir terminé le async void méthode et (de manière appropriée) lève une exception.

Ce que vous voulez probablement faire, c'est traiter les données simultanément , mais pas en parallèle . Le code parallèle ne devrait presque jamais être utilisé sur ASP.NET. Voici à quoi ressemblerait le code avec un traitement simultané asynchrone:

public async Task<MyResult> GetResult()
{
  MyResult result = new MyResult();

  var tasks = Methods.Select(method => ProcessAsync(method)).ToArray();
  string[] json = await Task.WhenAll(tasks);

  result.Prop1 = PopulateProp1(json[0]);
  ...

  return result;
}
63
Stephen Cleary

Alternativement, avec le AsyncEnumerator NuGet Package vous pouvez le faire:

using System.Collections.Async;

public async Task<MyResult> GetResult()
{
    MyResult result = new MyResult();

    await Methods.ParallelForEachAsync(async method =>
    {
        string json = await Process(method);    

        result.Prop1 = PopulateProp1(json);
        result.Prop2 = PopulateProp2(json);
    }, maxDegreeOfParallelism: 10);

    return result;
}

ParallelForEachAsync est une méthode d'extension.

6
Serge Semenov

Ahh d'accord. Je pense que je sais ce qui se passe maintenant. async method => un "async void" qui est "tirer et oublier" (déconseillé pour autre chose que les gestionnaires d'événements). Cela signifie que l'appelant ne peut pas savoir quand il est terminé ... Ainsi, GetResult retourne alors que l'opération est toujours en cours d'exécution. Bien que les détails techniques de ma première réponse soient incorrects, le résultat est le même ici: que GetResult renvoie alors que les opérations démarrées par ForEach sont toujours en cours d'exécution. La seule chose que vous pourriez vraiment faire est de ne pas await sur Process (pour que le lambda ne soit plus async) et d'attendre que Process termine chaque itération . Mais, cela utilisera au moins un thread de pool de threads pour ce faire et stressera donc légèrement le pool - probablement en utilisant ForEach inutile. Je n'utiliserais tout simplement pas Parallel.ForEach ...

5
Peter Ritchie