When I execute the following code:
using static System.Console;
var numbers = ParallelEnumerable.Range(0, 50);
WriteLine("\nNumbers divisible by 5 are:");
var divisibleBy5 = numbers
.AsParallel()
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
.Where(x => x % 5 == 0);
// .Where(x => x % 5 == 0)
//.ToList();
foreach (var number in divisibleBy5)
{
Write(number + "\t");
}
I can see the unordered result most of the time. Here is a sample output (Expected behavior):
Numbers divisible by 5 are:
0 15 30 40 5 20 35 45 10 25
Now I change the query a little bit (using ToList() now):
var divisibleBy5 = numbers
.AsParallel()
.WithExecutionMode(ParallelExecutionMode.ForceParallelism)
//.Where(x => x % 5 == 0);
.Where(x => x % 5 == 0)
.ToList();
Upon running this update, I always see the sorted output:
Numbers divisible by 5 are:
0 5 10 15 20 25 30 35 40 45
Questions:
Is this an expected behavior?
If so, why? Do we have any supporting docs for this?
What am I missing here? Can you please share your thoughts?
The explanation for the behavior is as follows:
The queries returned by
ParallelEnumerable.Range(),AsParallel()andWhere()are all of base typeParallelQuery<int>.Because of that, when you call the extension method
ToList()you are actually invokingParallelEnumerable.ToList<int>(this ParallelQuery<int>)rather thanEnumerable.ToList().ParallelEnumerable.ToList<int>()has a special case for ordered parallel enumerables that preserves order. This is mentioned briefly in the documentation page Query Operators and Ordering:Thus as long as the preceding parts of your query are ordered,
Where()andToList()are documented to preserve that order whileGetEnumerable()is not.That being said,
ParallelEnumerable.Range()is not documented to return an ordered parallel enumerable (the docs seem ambiguous to me), so we can't say for sure that your final list will be ordered. But we can explain whyToList()andforeachmight produce different orderings.To see how this works in practice, we can check the .NET 8 reference source and do some debugging. The source code for
ToList()can be found here:Debugging indicates that the concrete type of
sourceisWhereQueryOperator<int>withOrdinalIndexStateequal toOrdinalIndexState.Increasingwhen means thatGetEnumerator(ParallelMergeOptions.FullyBuffered)rather than justGetEnumerator()is called. The source for these methods is here:I.e. the difference between the enumerator used by
ToList()and the default enumerator used byforeachis the former passesParallelMergeOptions.FullyBuffered, which is documented as follows:This seems to be the cause of the inconsistency you are seeing.
To confirm that the behavior you are seeing is specific to
ParallelEnumerable.ToList(), I modified your query to cast toIEnumerable<int>before callingToList():When I did so, this resulted in a randomly ordered list.
You can see a demo fiddle showing all my variations of your
divisibleBy5query here: https://dotnetfiddle.net/qBccva.