Как вы используете AsParallel с асинхронными и ожидающими ключевыми словами?

Я искал пример кода для async и заметил несколько проблем с тем, как он был реализован. В то время как я смотрел на код, я задавался вопросом, было бы более эффективным перебирать список, используя как параллельный, а не просто циклически перебирать список.

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

Это первый способ сделать это

var tasks= Client.GetClients().Select(async p => await p.Initialize());

И это вторая

var tasks = Client.GetClients().AsParallel().Select(async p => await p.Initialize());

Правильно ли я предполагаю, что между ними нет разницы?

Полная программа может быть найдена ниже

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            RunCode1();
            Console.WriteLine("Here");
            Console.ReadLine();

            RunCode2();
            Console.WriteLine("Here");

            Console.ReadLine();

        }

        private async static void RunCode1()
        {
            Stopwatch myStopWatch = new Stopwatch();
            myStopWatch.Start();

            var tasks= Client.GetClients().Select(async p => await p.Initialize());

            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("Time ellapsed(ms): " + myStopWatch.ElapsedMilliseconds);
            myStopWatch.Stop();
        }
        private async static void RunCode2()
        {
            Stopwatch myStopWatch = new Stopwatch();
            myStopWatch.Start();
            var tasks = Client.GetClients().AsParallel().Select(async p => await p.Initialize());
            Task.WaitAll(tasks.ToArray());
            Console.WriteLine("Time ellapsed(ms): " + myStopWatch.ElapsedMilliseconds);
            myStopWatch.Stop();
        }
    }
    class Client
    {
        public static IEnumerable<Client> GetClients()
        {
            for (int i = 0; i < 100; i++)
            {
                yield return new Client() { Id = Guid.NewGuid() };
            }
        }

        public Guid Id { get; set; }

        //This method has to be called before you use a client
        //For the sample, I don't put it on the constructor
        public async Task Initialize()
        {
            await Task.Factory.StartNew(() =>
                                      {
                                          Stopwatch timer = new Stopwatch();
                                          timer.Start();
                                          while(timer.ElapsedMilliseconds<1000)
                                          {}
                                          timer.Stop();

                                      });
            Console.WriteLine("Completed: " + Id);
        }
    }
}

Ответ 1

Там должно быть очень мало различимых различий.

В вашем первом случае:

var tasks = Client.GetClients().Select(async p => await p.Initialize());

Выполняющий поток (по одному за раз) запускает Initialize для каждого элемента в списке клиентов. Initialize немедленно ставит очередь в пул потоков и возвращает незавершенную Task.

Во втором случае:

var tasks = Client.GetClients().AsParallel().Select(async p => await p.Initialize());

Выполняющий поток будет вилкой в пуле потоков и (параллельно) начнет выполнение Initialize для каждого элемента в списке клиентов. Initialize имеет такое же поведение: она немедленно ставит очередь в пул потоков и возвращает их.

Два тайминга почти идентичны, потому что вы только распараллеливаете небольшое количество кода: очередь процесса в пул потоков и возврат незавершенной Task.

Если Initialize выполняла более длительную (синхронную) работу до ее первого await, может иметь смысл использовать AsParallel.

Помните, что все async методы (и lambdas) запускаются синхронно (см. Официальный FAQ или мою собственную запись в intro).

Ответ 2

Там необычайно большое различие.

В следующем коде вы берете на себя обязательство выполнить разбиение. Другими словами, вы создаете один объект Task каждого элемента из IEnumerable<T> который возвращается из вызова GetClients():

var tasks= Client.GetClients().Select(async p => await p.Initialize());

Во втором случае вызов AsParallel внутренне собирается использовать экземпляры Task для выполнения разделов IEnumerable<T> и у вас будет начальная Task которая возвращается из lambda async p => await p.Initialize():

var tasks = Client.GetClients().AsParallel().
    Select(async p => await p.Initialize());

Наконец, вы ничего не делаете, используя async/ await здесь. Конечно, компилятор может оптимизировать это, но вы просто ожидаете метода, который возвращает Task а затем возвращает продолжение, которое ничего не делает через лямбда. Тем не менее, поскольку вызов Initialize уже возвращает Task, лучше всего держать его простым и просто делать:

var tasks = Client.GetClients().Select(p => p.Initialize());

Который вернет вам последовательность экземпляров Task.

Ответ 3

Чтобы улучшить приведенные выше ответы, это самый простой способ получить ожидаемое выполнение async/threaded:

var results = await Task.WhenAll(Client.GetClients(). Выберите (async p => p.Initialize()));

Это гарантирует, что он будет вращать отдельные потоки, и вы получите результаты в конце. Надежда помогает кому-то. Мне потребовалось некоторое время, чтобы понять это правильно, потому что это очень не очевидно, и функция AsParallel() кажется тем, что вы хотите, но не использует async/wait.