I found that combining Task.Run with plinq is extremely slow so I made a simple experiment:
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 inside plinq works well, finished in 14 milliseconds
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 inside task also ends in 14 milliseconds, but if I replace Task.Run inside with plinq like this:
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 );
It'll take 29 seconds to execute. Things get worse if scale
variable is larger.
Can anyone explain what happened in this case?
Edit:
I made another experiment:
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" );
} );
} );
}
And here is part of the result:
[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
Look into the timestamp between each tasks,you can find there is a mysterious 1000 milliseconds delay whenever it move to next task. I guess there is a mechanism in plinq or task that will pause for one second in some situation which slow down the process significantly.
Thanks to the explanation of @StephenCleary, now I understand the delay comes from the creation of thread. I tweak my experiment again and found that ForAll
method will block the task until all other ForAll
method in different tasks are completed.
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" );
} );
}
And the result is :
[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
I expect that ForAll
method should return immediately. Why is it block the task and the thread?