PLINQ and parallel queries

AsParallel splits work across cores, which only pays off for CPU-bound work over enough items with no shared state.

When parallelism helps

// PLINQ: the same operators, executed on the thread pool
var primes = Enumerable.Range(2, 5_000_000)
    .AsParallel()
    .WithDegreeOfParallelism(Environment.ProcessorCount)
    .Where(IsPrime)
    .ToArray();

// Preserving order costs synchronisation; drop it when order does not matter
var ordered = source.AsParallel().AsOrdered().Select(SlowPure).ToArray();

// Aggregation across partitions: pass a seed and a combine function
var total = big
    .AsParallel()
    .Aggregate(
        seed: 0L,
        updateAccumulatorFunc: (acc, item) => acc + item.Cost,
        combineAccumulatorsFunc: (left, right) => left + right,
        resultSelector: acc => acc);

// AsParallel is only worth it above a threshold; below it, the overhead wins
static IEnumerable<T> MaybeParallel<T>(IEnumerable<T> source, int threshold,
                                       Func<T, T> work)
{
    var asList = source as IList<T> ?? source.ToList();
    return asList.Count >= threshold
        ? asList.AsParallel().AsOrdered().Select(work)
        : asList.Select(work);
}
  • PLINQ pays for itself on CPU-bound work over tens of thousands of items. For a handful of items, the partitioning and merging cost more than the work.
  • IO-bound work is not helped by more threads. Use async for IO, and PLINQ for computation.
  • Parallel.ForEachAsync is the modern choice for asynchronous work with a bound, and it composes with cancellation tokens naturally.
  • Exceptions from several partitions are collected and thrown as an AggregateException, which changes your error handling.
  • Order preservation requires merging buffers, so the result arrives later than the first result of an unordered query.

Thread safety and measuring the gain

// WRONG: every partition writes to the same list. List<T> is not thread safe,
// and the lost updates are silent.
var results = new List<int>();
Enumerable.Range(0, 100_000)
    .AsParallel()
    .ForAll(i => results.Add(i * i));

// RIGHT: let the query produce the values; the framework merges them
var squares = Enumerable.Range(0, 100_000)
    .AsParallel()
    .Select(i => i * i)
    .ToArray();

// RIGHT for a shared accumulator: a thread-local state per partition
var sum = Enumerable.Range(0, 1_000_000)
    .AsParallel()
    .Aggregate(
        seed: 0L,
        updateAccumulatorFunc: (acc, i) => acc + i,
        combineAccumulatorsFunc: (l, r) => l + r,
        resultSelector: acc => acc);

// RIGHT for a truly shared structure: a concurrent collection
var bag = new System.Collections.Concurrent.ConcurrentBag<int>();
Enumerable.Range(0, 100_000).AsParallel().ForAll(i => bag.Add(i * i));

// A delegate that is not pure is a bug waiting for a race
// WRONG: increments a captured variable
var count = 0;
source.AsParallel().ForAll(_ => count++);
WorkloadParallel helps?Better tool
CPU-heavy computation, many itemsYesPLINQ or Parallel.ForEach
A few long CPU tasksSometimesTask.Run per task
IO-bound callsNoasync and Parallel.ForEachAsync
Sorting a large arrayMarginalAn optimised single-threaded sort
Small collectionsNo, it is slowerPlain LINQ
Work with a shared mutable accumulatorOnly with careA partition-local accumulator
⚠️
Measure the parallel version against the sequential one on the same machine and the same data. Parallel queries frequently run slower under load on a server that is already busy, because they compete for the same cores as the request handling that is paying for the query.

FAQ

Does AsParallel preserve order?
No, unless you add AsOrdered. Without it the results come out in an arbitrary order, which is fine for an aggregate and wrong for anything positional.
Is PLINQ usable on EF Core queries?
No. AsParallel over an IQueryable forces the query to execute and the remaining work to happen in memory across threads. Filter and project in SQL first, then parallelise the in-memory computation if it is genuinely CPU-bound.

Query performance and avoiding N+1 Async LINQ and streaming with IAsyncEnumerable

Last refreshed 2026-09-18.