Почему сочетание Task.Run и plinq так медленно? - PullRequest
1 голос
/ 10 мая 2019

Я обнаружил, что объединение Task.Run с plinq чрезвычайно медленно, поэтому я провел простой эксперимент:

int scale = 32;

Enumerable.Range( 0, scale ).AsParallel().ForAll( i => {
    Enumerable.Range( 0, scale ).AsParallel().ForAll( j =>
    {
        for ( int k = 0; k < scale; k++ ) { }
    } );
} );

plinq внутри plinq работает хорошо, завершается за 14 миллисекунд

int scale = 32;

Task[] tasks = Enumerable.Range( 0, scale ).Select( i => Task.Run( async () =>
{
    Task[] _tasks = Enumerable.Range( 0, scale ).Select( j => Task.Run( () =>
    {
        for ( int k = 0; k < scale; k++ ) { }
    } ) ).ToArray();
    await Task.WhenAll( _tasks );
} ) ).ToArray();

await Task.WhenAll( tasks );

Задача внутри задачи также заканчивается за 14 миллисекунд, но если я заменю Task.Run внутри на plinq следующим образом:

int scale = 32;

Task[] tasks = Enumerable.Range( 0, scale ).Select( i => Task.Run( () =>
{
    Enumerable.Range( 0, scale ).AsParallel().ForAll( j =>
    {
        for ( int k = 0; k < scale; k++ ) { }
    } );
} ) ).ToArray();

await Task.WhenAll( tasks );

Выполнение займет 29 секунд.Ситуация ухудшается, если scale переменная больше.

Может кто-нибудь объяснить, что произошло в этом случае?


Редактировать:

Я провел еще один эксперимент:

static async Task Main( string[] args )
{
    Stopwatch stopwatch = Stopwatch.StartNew();

    int scale = 8;

    Task[] tasks = Enumerable.Range( 0, scale ).Select( id => Run( scale, id ) ).ToArray();

    await Task.WhenAll( tasks );

    Console.WriteLine( $"ElapsedTime={stopwatch.ElapsedMilliseconds}ms" );
}

static Task Run( int scale, int id )
{
    return Task.Run( () =>
    {
        Enumerable.Range( 0, scale ).AsParallel().ForAll( j =>
        {
            for ( int k = 0; k < scale; k++ )
            {

            }

            Console.WriteLine( $"[{DateTimeOffset.Now.ToUnixTimeMilliseconds()}]Task {id} for loop {j} end" );
        } );
    } );
}

И вот часть результата:

[1557475215796]Task 0 for loop 6 end
[1557475215796]Task 0 for loop 7 end
[1557475216776]Task 4 for loop 0 end
[1557475216776]Task 4 for loop 1 end
[1557475216777]Task 4 for loop 2 end
[1557475216777]Task 4 for loop 3 end
[1557475216778]Task 4 for loop 4 end
[1557475216778]Task 4 for loop 5 end
[1557475216779]Task 4 for loop 6 end
[1557475216780]Task 4 for loop 7 end
[1557475217774]Task 5 for loop 0 end
[1557475217774]Task 5 for loop 1 end
[1557475217775]Task 5 for loop 2 end

Посмотрите на временную метку между каждой задачей, вы можете обнаружить загадочную задержку в 1000 миллисекунд при каждом переходе к следующей задаче.Я предполагаю, что в plinq или задании есть механизм, который в какой-то ситуации приостанавливается на одну секунду, что значительно замедляет процесс.


Благодаря объяснению @StephenCleary теперь я понимаю, что задержка происходит из-засоздание темы.Я снова настраиваю свой эксперимент и обнаруживаю, что метод ForAll будет блокировать задачу до тех пор, пока все другие методы ForAll в других задачах не будут завершены.

static Task Run( int scale, int id )
{
    return Task.Run( () =>
    {
        Enumerable.Range( 0, scale ).AsParallel().ForAll( j =>
        {
            for ( int k = 0; k < scale; k++ )
            {

            }

            Console.WriteLine( $"[{DateTimeOffset.Now.ToUnixTimeMilliseconds()}]Task {id} for loop {j} end, thread count = {Process.GetCurrentProcess().Threads.Count}" );
        } );
        Console.WriteLine( $"[{DateTimeOffset.Now.ToUnixTimeMilliseconds()}]Task {id} finished" );
    } );
}

И в результате:

[1557478553656]Task 6 for loop 6 end, thread count = 19
[1557478553657]Task 6 for loop 7 end, thread count = 19
[1557478554645]Task 7 for loop 0 end, thread count = 20
[1557478554647]Task 7 for loop 1 end, thread count = 20
[1557478554649]Task 7 for loop 2 end, thread count = 20
[1557478554651]Task 7 for loop 3 end, thread count = 20
[1557478554653]Task 7 for loop 4 end, thread count = 20
[1557478554655]Task 7 for loop 5 end, thread count = 20
[1557478554657]Task 7 for loop 6 end, thread count = 20
[1557478554659]Task 7 for loop 7 end, thread count = 20
[1557478555644]Task 1 finished
[1557478555644]Task 0 finished
[1557478555644]Task 3 finished
[1557478555644]Task 2 finished
[1557478555644]Task 4 finished
[1557478555644]Task 6 finished
[1557478555644]Task 5 finished
[1557478555644]Task 7 finished

Я ожидаю, что ForAll метод должен вернуться немедленно.Почему это блокирует задачу и поток?

1 Ответ

2 голосов
/ 10 мая 2019

Проблема явно в вашем коде, давайте рассмотрим различные фрагменты кода, особенно те, которые используют Task, так как PLinq внутри PLinq прост, и в значительной степени использует все возможные потоки / ядра для быстрой обработки насколько это возможно, не будет большого сдвига контекста, поскольку обработка выполняется в памяти и быстро. Infact PLinq сам будет управлять / контролировать число Параллельный вызов в отличие от Task.Run, который относительно независим.

Фрагмент 1

int scale = 32;

Task[] tasks = Enumerable.Range( 0, scale ).Select( i => Task.Run( async () =>
{
    Task[] _tasks = Enumerable.Range( 0, scale ).Select( j => Task.Run( () =>
    {
        for ( int k = 0; k < scale; k++ ) { }
    } ) ).ToArray();
    await Task.WhenAll( _tasks );
} ) ).ToArray();

await Task.WhenAll( tasks );
  • Здесь ваша полная обработка находится в памяти, и каждая внешняя задача асинхронно планирует внутренние циклы, в то время как сама задача не блокирует поток и ожидает завершения внутренних задач, поэтому внешние Task.Run будут уведомляться асинхронно, когда внутренние Task.Run завершено

Теперь, что происходит с более медленным кодом, давайте рассмотрим

Фрагмент 2

int scale = 32;

Task[] tasks = Enumerable.Range( 0, scale ).Select( i => Task.Run( () =>
{
    Enumerable.Range( 0, scale ).AsParallel().ForAll( j =>
    {
        for ( int k = 0; k < scale; k++ ) { }
    } );
} ) ).ToArray();

await Task.WhenAll( tasks );
  • Здесь каждый Task.Run не асинхронно передает запрос на внутренний вызов PLinq, и что произойдет, если поток, вызванный Task.Run, будет заблокирован для завершения внутреннего PLinq, и это является основным источником проблемы здесь, таким образом, приводит к высокой конкуренции.

Как объяснено выше, существует существенная разница между тем, как Task.Run, вызывающий PLinq, отличается от PLinq, вызывающим PLinq, поэтому ключ заключается в понимании того, как эти различные API работают индивидуально, и каково влияние объединения они будут работать вместе, как того требует ваш код.

...