为什么结合Task.Run和plinq这么慢?

时间:2019-05-10 04:03:58

标签: c# task-parallel-library plinq

我发现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 个答案:

答案 0 :(得分:2)

问题显然在您的代码中,让我们回顾一下各种代码片段,尤其是使用Task的代码片段,因为PLinq中的PLinq很简单,几乎所有可能的线程都在使用/内核尽可能快地处理,因为处理在内存中且速度很快,因此上下文转移不会太多。实际上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都异步安排内部循环,而Task本身不会阻塞线程并等待内部Task完成,因此外部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的情况,这是内部PLinq的主要来源出现在这里,从而导致竞争激烈。

如上所述,Task.Run调用PLinqPLinq调用PLinq的方式之间有很大的不同,因此关键在于理解这些不同的API如何工作单独地进行,以及将它们组合在一起以达到您的代码期望的作用。