Понимание параллельного программирования на C# с помощью асинхронных примеров

Я пытаюсь понять параллельное программирование и хочу, чтобы мои методы async работали в нескольких потоках. Я что-то написал, но это не работает, как я думал.

Код

public static async Task Main(string[] args)
{
    var listAfterParallel =  RunParallel(); // Running this function to return tasks
    await Task.WhenAll(listAfterParallel); // I want the program exceution to stop until all tasks are returned or tasks are completed
    Console.WriteLine("After Parallel Loop"); // But currently when I run program, after parallel loop command is printed first
    Console.ReadLine();
}

public static async Task<ConcurrentBag<string>> RunParallel()
{
     var client = new System.Net.Http.HttpClient();
     client.DefaultRequestHeaders.Add("Accept", "application/json");
     client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
     var list = new List<int>();
     var listResults = new ConcurrentBag<string>();
     for (int i = 1; i < 5; i++)
     {
       list.Add(i);
     }
     // Parallel for each branch to run await commands on multiple threads. 
     Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, async (index) =>
     {
         var response = await client.GetAsync("posts/" + index);
         var contents = await response.Content.ReadAsStringAsync();
         listResults.Add(contents);
         Console.WriteLine(contents);
     });
     return listResults;
}

Я хотел бы, чтобы функция RunParallel завершилась до того, как будет напечатано «После параллельного цикла». Также я хочу, чтобы мой метод получения сообщений выполнялся в нескольких потоках.

Любая помощь будет оценена по достоинству!


person Learn AspNet    schedule 04.10.2019    source источник
comment
Параллельность — это не то же самое, что асинхронность. Один работает в нескольких потоках, другой не блокирует поток, пока он чего-то ждет (часто IO, но, возможно, для другого потока, чтобы завершить некоторую работу). Если вы хотите, чтобы ввод-вывод происходил параллельно, вам просто нужно собрать задачи и избавиться от Parallel.ForEach.   -  person juharr    schedule 04.10.2019
comment
async/await не предназначены для параллелизма, они помогают с асинхронными операциями. Parallel.ForEach предназначен для параллелизма данных (локальная обработка 100K/1M элементов) и определенно не предназначен для асинхронной работы. Фактически, он не может ожидать каких-либо асинхронных операций. Этот код будет запускать все запросы одновременно и никогда не будет получать результаты.   -  person Panagiotis Kanavos    schedule 04.10.2019
comment
В любом случае асинхронные операции уже выполняются в другом потоке или не беспокоят своего создателя, пока не закончатся. Вы можете использовать, например, var results = Task.WhenAll(Enumerable.Range(1,5).Select(i=>client.GetStringAsync($"posts/{i}")));, чтобы запустить все 5 задач и дождаться их результатов без блокировки.   -  person Panagiotis Kanavos    schedule 04.10.2019
comment
@PanagiotisKanavos будет ли это работать в несколько потоков?   -  person Learn AspNet    schedule 04.10.2019
comment
@PanagiotisKanavos Что бы вы использовали вместо Parallel.ForEach? Можем ли мы использовать ActionBlock для параллелизма данных, а также для асинхронной работы?   -  person Learn AspNet    schedule 04.10.2019
comment
@LearnAspNet Parallel.ForEach требует много ресурсов ЦП. Не рекомендуется использовать на стороне сервера, это может просто вывести машину из строя.   -  person OlegI    schedule 04.10.2019
comment
Асинхронные операции @PanagiotisKanavos (я имею в виду async/await) не всегда выполняются в отдельных потоках. Это зависит от того, сколько времени потребуется на выполнение операции. Но в большинстве случаев да, он работает в отдельном потоке.   -  person OlegI    schedule 04.10.2019
comment
@OlegI Я хочу, чтобы он работал параллельно. Как я могу убедиться, что он работает на нескольких ядрах, если нужно обработать миллион записей?   -  person Learn AspNet    schedule 04.10.2019


Ответы (2)


Здесь происходит то, что вы никогда не ждете завершения блока Parallel.ForEach — вы просто возвращаете сумку, в которую он в конечном итоге закачается. Причина этого в том, что поскольку Parallel.ForEach ожидает Action делегатов, вы создали лямбду, которая возвращает void, а не Task. Несмотря на то, что методы async void допустимы, они обычно продолжают свою работу в новом потоке и возвращаются к вызывающей стороне, как только они await создают задачу, и поэтому метод Parallel.ForEach считает, что обработчик выполнен, даже если он выбрасывает оставшуюся работу в отдельный поток. нить.

Вместо этого используйте здесь синхронный метод;

Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, index => 
{
    var response = client.GetAsync("posts/" + index).Result;

    var contents = response.Content.ReadAsStringAsync().Result;
    listResults.Add(contents);
    Console.WriteLine(contents);
});

Если вам абсолютно необходимо использовать await внутри, заверните его в Task.Run(...).GetAwaiter().GetResult();

Parallel.ForEach(list, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, index => Task.Run(async () =>
{
    var response = await client.GetAsync("posts/" + index);

    var contents = await response.Content.ReadAsStringAsync();
    listResults.Add(contents);
    Console.WriteLine(contents);
}).GetAwaiter().GetResult();

Однако в этом случае Task.run обычно переходит в новый поток, поэтому мы лишили большей части контроля Parallel.ForEach; лучше использовать async до конца;

var tasks = list.Select(async (index) => {
        var response = await client.GetAsync("posts/" + index);

        var contents = await response.Content.ReadAsStringAsync();
        listResults.Add(contents);
        Console.WriteLine(contents);
    });
await Task.WhenAll(tasks);

Поскольку Select ожидает Func<T, TResult>, он будет интерпретировать лямбда async без return как метод async Task вместо async void и, таким образом, даст нам то, что мы можем явно await

person David    schedule 04.10.2019
comment
Мне нравится ваш подход, но можете ли вы подтвердить, что функция list.select будет работать в нескольких потоках и будет работать параллельно? Я также хочу указать, сколько ядер он может использовать. - person Learn AspNet; 04.10.2019
comment
Параллельный запуск в любое время, когда вы используете Задачи, контролируется контекстом синхронизации. Я обновлю, чтобы показать решение с использованием Select и AsParallel для создания тех же элементов управления. - person David; 04.10.2019
comment
Кроме того, можем ли мы использовать блоки действий для асинхронных методов, потому что я также хочу передать количество ядер/потоков, которые он может использовать. - person Learn AspNet; 04.10.2019
comment
Если вы хотите использовать асинхронные методы, но определить максимальное распараллеливание, вам нужно будет возиться с вашим локальным TaskScheduler, что является проблемой с большей областью действия из-за того, что TaskScheduler является абстрактным. - person David; 04.10.2019

Взгляните на это: Темы нет

Когда вы делаете несколько одновременных веб-запросов, тяжелую работу выполняет не ваш процессор. Это процессор веб-сервера, который обслуживает ваши запросы. Ваш процессор ничего не делает в это время. Это не особое «состояние ожидания» или что-то в этом роде. Аппаратное обеспечение внутри вашей коробки, которое работает, — это ваша сетевая карта, которая записывает данные в вашу оперативную память. Когда ответ будет получен, ваш ЦП будет уведомлен о поступивших данных, чтобы он мог что-то с ними сделать.

Вам нужен параллелизм, когда у вас есть тяжелая работа внутри вашей коробки, а не когда вы хотите, чтобы тяжелая работа выполнялась внешним миром. С точки зрения вашего процессора, даже ваш жесткий диск является частью внешнего мира. Таким образом, все, что относится к веб-запросам, применимо и к запросам, нацеленным на файловые системы и базы данных. Эти рабочие нагрузки называются привязкой ввода-вывода. , чтобы отличать их от так называемых рабочих нагрузок, связанных с процессором.

Для рабочих нагрузок, связанных с вводом-выводом, платформа .NET предлагает инструмент асинхронного Task. В библиотеках есть несколько API, которые возвращают Task объектов. Для достижения параллелизма обычно вы запускаете несколько задач, а затем await выполняете их с помощью Task.WhenAll. Существуют также более продвинутые инструменты, такие как Библиотека потоков данных TPL, созданная на основе Tasks. Он предлагает такие возможности, как буферизация, пакетная обработка, настройка максимальной степени параллелизма и многое другое.

person Theodor Zoulias    schedule 05.10.2019