Процесс не работает параллельно с использованием ParallelForEachAsync - PullRequest
0 голосов
/ 07 февраля 2019

Я тестирую работающий Python через Process.Start параллельно

Моя машина имеет процессор 2,8 ГГц с 4 ядрами и 8 логическими процессорами

Мое основное консольное приложениекак показано ниже

    static void Main(string[] args) => MainAsync(args).GetAwaiter().GetResult();

    static async Task MainAsync(string[] args)
    {            
        var startTime = DateTime.UtcNow;
        Console.WriteLine($"Execution started at {DateTime.UtcNow:T}");

        await ExecuteInParallelAsync(args).ConfigureAwait(false);
        Console.WriteLine($"Executions completed at {DateTime.UtcNow:T}");
        var endTime = DateTime.UtcNow;

        var duration = (endTime - startTime);
        Console.WriteLine($"Execution took {duration.TotalMilliseconds} milliseconds {duration.TotalSeconds} seconds");

        Console.WriteLine("Press Any Key to close");
        Console.ReadKey();            
    }

Где ExecuteInParallelAsync - метод, выполняющий эту работу ...

    private static async Task ExecuteInParallelAsync(string[] args)
    {
        var executionNumbers = new List<int>();
        var executions = 5;

        for (var executionNumber = 1; executionNumber <= executions; executionNumber++)
        {
            executionNumbers.Add(executionNumber);
        }

        await executionNumbers.ParallelForEachAsync(async executionNumber =>
        {
             Console.WriteLine($"Execution {executionNumber} of {executions} {DateTime.UtcNow:T}");
            ExecuteSampleModel();
            Console.WriteLine($"Execution {executionNumber} complete {DateTime.UtcNow:T}");
        }).ConfigureAwait(false);
    }

ExecuteSampleModel запускает модель Python ...

    IModelResponse GetResponse()
    { 
        _actualResponse = new ModelResponse();

        var fileName = $@"main.py";

        var p = new Process();
        p.StartInfo = new ProcessStartInfo(@"C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python36_64\python.exe", fileName)
        {
            WorkingDirectory = RootFolder,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };
        p.Start();

        _actualResponse.RawResponseFromModel = p.StandardOutput.ReadToEnd();
        p.WaitForExit();

        return _actualResponse;
    }

Как вы можете видеть, я прошу эту модель быть выполненной 5 раз

Когда я использую отладчик, это выглядит так, как будто даже при использовании ParalellForEach (представленного пакетом AsyncEnumerator) она не запускается параллельно

Я думал, что каждая итерация выполняется в своем собственном потоке?

Каждое выполнение модели Python занимает 5 секунд.

Параллельно с выполнением, я ожидал бы, что весь процесс будет выполненчерез 15 секунд или около того, но на самом деле это занимает 34 секунды

Console.WriteLines, добавленные до и после вызова GetResponse, показывают, что первый вызов начинается и выполняется вполный, затем запускается второй и т. д.

Это как-то связано со мной, вызывая Process.Start?

Может кто-нибудь увидеть что-то не так с этим?

Пол

1 Ответ

0 голосов
/ 08 февраля 2019

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

// some preparations
...
var itemIndex = 0L;
while (await enumerator.MoveNextAsync(cancellationToken).ConfigureAwait(false))
{
    ...
    Task itemActionTask = null;
    try
    {
        itemActionTask = asyncItemAction(enumerator.Current, itemIndex);
    }
    catch (Exception ex)
    {
       // some exception handling
    }
    ...
    itemIndex++;
}

, где asyncItemAction имеет тип Func<T, long, Task>, и это обертка вокруг пользовательскогоасинхронное действие с типом Func<T, Task>, которое передается в качестве параметра в вызов ParallelForEachAsync (оболочка добавляет функции индексирования).Код цикла просто вызывает это действие, чтобы получить задачу, которая представляла бы асинхронную операцию, ожидающую завершения ее выполнения.В случае данного примера кода пользовательское действие

async executionNumber =>
{
     Console.WriteLine($"Execution {executionNumber} of {executions}{DateTime.UtcNow:T}");
     ExecuteSampleModel();
     Console.WriteLine($"Execution {executionNumber} complete {DateTime.UtcNow:T}");
}

не содержит асинхронного кода, но префикс async позволяет компилятору генерировать конечный автомат с методом, который возвращает некоторое значение Task, которое делает этот код совместимым (из синтаксисаточка зрения) с вызовом пользовательского действия внутри цикла.Важно то, что код внутри цикла ожидает, что эта операция будет асинхронной, что подразумевает, что операция неявно разбивается на синхронную часть, которая будет выполняться вместе с вызовом asyncItemAction(enumerator.Current, itemIndex) и хотя бы одной (одна или несколько в зависимости от числа awaits).внутри) асинхронные части, которые могут быть выполнены во время итерации по другим элементам цикла.Следующий псевдокод дает представление об этом:

 {
     ... synchronous part
     await SomeAsyncOperation();
     ... asynchronous part
 } 

В этом конкретном случае вообще нет асинхронной части в пользовательском действии, что означает, что вызов

 itemActionTask = asyncItemAction(enumerator.Current, itemIndex);

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

Вот почему помогает отключение асинхронности в коде и использование простого параллелизма.

...