Подтвердить что ты не робот

Parallel.ForEach и async-wait

У меня был такой метод:

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;
}

Затем я решил использовать 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;
}

Но теперь у меня есть ошибка:

Асинхронный модуль или обработчик завершен, пока асинхронная операция еще не выполнена.

4b9b3361

Ответ 1

async не работает с ForEach. В частности, ваша лямбда async преобразуется в метод async void. Существует ряд причин, чтобы избежать async void (как я описываю в статье MSDN); одним из них является то, что вы не можете легко обнаружить, когда лямбда async завершена. ASP.NET увидит, что ваш код возвращается без завершения метода async void и (соответственно) выдает исключение.

То, что вы, вероятно, захотите сделать, это обрабатывать данные одновременно, а не параллельно. Параллельный код почти никогда не должен использоваться на ASP.NET. Вот как выглядит код с асинхронной параллельной обработкой:

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;
}

Ответ 2

Ах, ладно. Кажется, я знаю, что происходит сейчас. async method => "асинхронная пустота", которая является "огнем и забыть" (не рекомендуется ни для чего, кроме обработчиков событий). Это означает, что вызывающий абонент не может знать, когда он будет завершен. Итак, GetResult возвращается во время выполнения операции. Хотя технические данные моего первого ответа неверны, результат здесь тот же: GetResult возвращается, а операции, запущенные с помощью ForEach, все еще работают. Единственное, что вы могли бы сделать, это не await на Process (так что лямбда больше не async) и дождитесь Process для завершения каждой итерации. Но для этого потребуется хотя бы один поток потока нитей, и, таким образом, немного стесняйтесь в пуле - вероятно, использование ForEach бессмысленно. Я бы просто не использовал Parallel.ForEach...

Ответ 3

В качестве альтернативы, с AsyncEnumerator NuGet Package вы можете сделать это:

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 - метод расширения.