我发现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
方法应该立即返回。为什么会阻塞任务和线程?
答案 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.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
调用PLinq
与PLinq
调用PLinq
的方式之间有很大的不同,因此关键在于理解这些不同的API如何工作单独地进行,以及将它们组合在一起以达到您的代码期望的作用。