📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

๐Ÿ“š Interview question

What is PLINQ, when does AsParallel help, and when does it make things worse?

Parallel LINQ partitioning, degree of parallelism, ordering, merge options, cancellation, exceptions, and why it is for CPU-bound work only.

Mid-Level Theory Intermediate data-parallelism parallel-linq performance plinq

๐ŸŽฏ Key points

  • PLINQ partitions LINQ to Objects queries across cores
  • Helps for large in-memory, CPU-bound, expensive per-item work
  • Hurts for small or cheap work, I/O, and busy servers
  • Results are unordered unless AsOrdered is used
  • WithDegreeOfParallelism and WithCancellation control execution
  • Exceptions arrive as AggregateException
  • Lambdas must be free of shared mutable state

โฑ 30-second answer

PLINQ (Parallel LINQ) runs LINQ to Objects queries on multiple cores: source.AsParallel().Where(...).Select(...) partitions the input and processes partitions on ThreadPool threads, then merges results. It helps for CPU-bound work on large in-memory collections where each item is expensive. It makes things worse for small inputs, cheap per-item work, I/O-bound work (threads block), lambdas with shared mutable state, and server code where all cores are already busy with requests. Results are unordered unless you add AsOrdered(), and exceptions arrive as an AggregateException.

๐Ÿ—ฃ Full answer (senior)

How I would answer this in an interview

"PLINQ, Parallel LINQ, runs LINQ to Objects queries on multiple cores. source.AsParallel() turns the sequence into a ParallelQuery<T>. The input is partitioned, partitions are processed on ThreadPool threads (by default up to the processor count), and the results are merged back. It helps when the work is CPU-bound, the collection is large and in memory, and each item is expensive and pure: hashing, parsing, simulations, scoring. It makes things worse when the input is small or the per-item work is cheap (partitioning and merging cost more than they save), when the lambda does I/O (each partition blocks a pool thread), when lambdas share mutable state (races), and in ASP.NET Core request paths, where the cores are already busy serving other requests. Results are unordered unless you add AsOrdered(), exceptions arrive as an AggregateException, and you control it with WithDegreeOfParallelism and WithCancellation. I always benchmark before and after."


1. How it works

int[] (5M items)
     โ”‚ AsParallel()
     โ–ผ
partitioning: range (arrays/lists) or chunk (other IEnumerable)
     โ”‚
 [p1]  [p2]  [p3]  [p4] ... on pool threads (+ the calling thread)
     โ”‚
merge: buffered (default) / not buffered / fully buffered; ordered only if AsOrdered()
     โ–ผ
consumer (Count, ToArray, foreach, ForAll)

2. Operators that control execution

Operator Effect
WithDegreeOfParallelism(n) Max concurrent tasks (leave headroom: ProcessorCount - 1)
AsOrdered() / AsUnordered() Preserve input order (buffering cost) / drop the requirement
WithCancellation(ct) Stop early (throws OperationCanceledException)
WithMergeOptions(...) NotBuffered streams results sooner; FullyBuffered maximises throughput
WithExecutionMode(ForceParallelism) Override PLINQ's decision to run some query shapes sequentially
ForAll(action) Process results in parallel without merging back (side effects must be thread-safe)

3. When it helps vs hurts

Helps Hurts
Large in-memory arrays/lists Small collections
Expensive pure per-item functions (ยตsโ€“ms each) Cheap per-item work (ns)
Batch jobs, CLI tools, desktop apps with idle cores ASP.NET Core request handling (cores already busy)
Pure functions, results returned from the query Lambdas mutating shared collections/fields
CPU-bound I/O-bound (HTTP, DB, files)
IEnumerable in memory EF Core IQueryable: AsParallel() executes the query and pulls all rows into memory first

4. Exceptions and cancellation

Situation Behaviour
One partition throws Query stops as soon as practical; other partitions stop
Several throw All collected in an AggregateException
WithCancellation cancelled OperationCanceledException

5. Choosing the right parallel tool

Workload Tool
CPU-bound query over in-memory data PLINQ
CPU-bound loop with side effects / per-thread state Parallel.For / Parallel.ForEach
Many I/O operations with a concurrency limit Parallel.ForEachAsync, or Task.WhenAll + SemaphoreSlim
Numeric kernels SIMD: Vector<T>, TensorPrimitives
Multi-stage pipelines Channels / TPL Dataflow

Production example

Our [risk platform] computed scores for [5 million] in-memory positions with a pure function taking about [20 ยตs] each: [~100 s] sequentially in a nightly job. Adding AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount - 1) brought it to [about 16 s] on 8 cores. But the scoring code was shared, and the same change landed on an API endpoint that scored positions on demand: under load, API p99 doubled, because the cores and pool threads were already busy with requests and PLINQ just added contention. I kept PLINQ in the batch job, removed it from the request path, and changed the API to read precomputed scores. We documented "no parallel loops in request paths" and added a load test to catch regressions.

๐Ÿ” What the interviewer is checking

Checks data-parallelism judgement.

Green flags

  • Explains partitioning and merging.
  • Limits use to CPU-bound, large workloads.
  • Knows ordering, cancellation and AggregateException.
  • Warns against I/O and server request paths.

Red flags

  • Adds AsParallel() to EF queries or HTTP calls.
  • Mutates shared lists in parallel lambdas.

๐Ÿ“š Learn in depth

1. Cost model

speed-up โ‰ค 1 / ((1 - p) + p / n)            Amdahl: p = parallel fraction, n = cores
real speed-up = Amdahl โˆ’ partitioning cost โˆ’ merge cost โˆ’ contention โˆ’ GC

For tiny per-item work, partitioning and merging dominate and PLINQ is slower than sequential LINQ. Rule of thumb: if the whole sequential query takes only a few milliseconds, stay sequential. Benchmark with BenchmarkDotNet on production-like hardware.

2. Partitioning strategies

Source Default partitioning Notes
Array / IList<T> Range (static, contiguous ranges) Low overhead; uneven work per item can leave cores idle
Other IEnumerable<T> Chunk (dynamic, growing chunk sizes, with locking) Load-balances, more overhead
Custom Partitioner.Create(...) E.g. load-balanced partitioning for arrays with uneven work

3. Ordering

AsOrdered() makes each partition keep indices and the merge reorders. Operators like Take(10) with AsOrdered() return the first ten in source order; without it, any ten. Use AsUnordered() after the order-sensitive part to regain throughput.

4. Thread-safe aggregation

// WRONG: shared List<T> mutated from many threads
var results = new List<int>();
numbers.AsParallel().ForAll(n => { if (IsPrime(n)) results.Add(n); });   // lost items / exceptions

// RIGHT: let the query produce results
int[] primes = numbers.AsParallel().Where(IsPrime).ToArray();

// RIGHT: per-partition aggregation
long sum = numbers.AsParallel().Aggregate(
    seed: 0L,
    updateAccumulatorFunc: (acc, n) => acc + n,
    combineAccumulatorsFunc: (a, b) => a + b,
    resultSelector: total => total);

5. Why not in ASP.NET Core requests

Under load, every core already runs request work. PLINQ in one request:

  • takes extra pool threads, competing with other requests;
  • increases context switching and cache misses;
  • can trigger thread injection.

Result: one request gets faster in isolation, and everyone's p99 gets worse under load. Move heavy CPU work to background jobs or precompute.

6. Version notes

Version Change
.NET Framework 4.0 PLINQ, Parallel class
.NET 6 Parallel.ForEachAsync (async alternative for I/O)
.NET 8 TensorPrimitives for vectorised numeric work
.NET 10 PLINQ API unchanged

7. Try it yourself

  1. Run the code example and compare timings; then reduce to 1..1,000 and see PLINQ lose.
  2. Use ForAll with a shared List<int> and observe exceptions or missing items; fix with ToArray().
  3. Compare AsOrdered().Take(10) with unordered Take(10).
  4. Add Thread.Sleep(1) (simulated I/O) inside the lambda and watch thread count climb with dotnet-counters.
  5. Benchmark range vs load-balanced partitioning (Partitioner.Create(array, true)) for uneven work.
  6. Load-test an endpoint with and without PLINQ and compare p99.

๐Ÿ’ป Code

using System.Diagnostics;

int[] numbers = Enumerable.Range(1, 3_000_000).ToArray();

var sw = Stopwatch.StartNew();
int sequential = numbers.Count(IsPrime);
Console.WriteLine($"Sequential: {sequential} primes in {sw.ElapsedMilliseconds} ms");

using var cts = new CancellationTokenSource();
sw.Restart();
int parallel = numbers
    .AsParallel()
    .WithDegreeOfParallelism(Math.Max(1, Environment.ProcessorCount - 1))
    .WithCancellation(cts.Token)
    .Count(IsPrime);
Console.WriteLine($"PLINQ: {parallel} primes in {sw.ElapsedMilliseconds} ms");

// Ordered output when order matters.
var firstTen = numbers.AsParallel().AsOrdered().Where(IsPrime).Take(10).ToArray();
Console.WriteLine(string.Join(", ", firstTen));

// Exceptions arrive as AggregateException.
try
{
    _ = numbers.AsParallel().Select(n => n == 1_234_567 ? throw new InvalidDataException("bad") : n).ToArray();
}
catch (AggregateException ex)
{
    Console.WriteLine($"Failed: {ex.InnerExceptions[0].Message}");
}

static bool IsPrime(int n)
{
    if (n < 2) return false;
    for (int d = 2; d * d <= n; d++)
        if (n % d == 0) return false;
    return true;
}

โš  Common mistakes

1. PLINQ for I/O

What happens: urls.AsParallel().Select(u => http.GetStringAsync(u).Result). Why it is wrong: each partition blocks a pool thread while waiting โ†’ starvation. Do instead: Parallel.ForEachAsync with MaxDegreeOfParallelism, or throttled Task.WhenAll.

2. Mutating shared collections

What happens: list.Add or dict[key] = ... inside parallel lambdas. Why it is wrong: not thread-safe โ†’ lost items, corrupted state, exceptions. Do instead: return results from the query, use Aggregate with per-partition seeds, or concurrent collections.

3. Small or cheap workloads

What happens: AsParallel() on 100 items with trivial lambdas. Why it is wrong: overhead exceeds benefit. Do instead: benchmark; stay sequential for small total work.

4. PLINQ in ASP.NET Core request paths

What happens: a controller parallelises scoring per request. Why it is wrong: competes with other requests; tail latency worsens under load. Do instead: sequential per request; heavy CPU work in background jobs.

5. AsParallel() on EF Core queries

What happens: db.Orders.AsParallel().Where(...). Why it is wrong: executes the full query and pulls all rows into memory first. Do instead: filter in the database; parallelise only in-memory CPU work.

6. Assuming order is preserved

What happens: code relies on output order matching input. Why it is wrong: PLINQ is unordered by default. Do instead: AsOrdered() where order matters.

7. Catching the wrong exception type

What happens: catch (InvalidDataException) around a PLINQ query. Why it is wrong: exceptions arrive wrapped in AggregateException. Do instead: catch AggregateException and inspect InnerExceptions.

8. Using all cores in shared environments

What happens: default degree of parallelism on a box running other services. Why it is wrong: starves co-located work. Do instead: WithDegreeOfParallelism to leave headroom.

9. Impure lambdas with hidden shared state

What happens: lambdas use a shared Random, StringBuilder or cache. Why it is wrong: races and corrupted results. Do instead: Random.Shared, thread-local helpers, or pure functions.

10. Not benchmarking

What happens: AsParallel() added on intuition. Why it is wrong: often slower; sometimes faster only on dev machines. Do instead: BenchmarkDotNet on production-like hardware, plus a load test for server code.

๐Ÿ› Senior insight: trade-offs

PLINQ is a batch-processing tool. I reach for it in jobs and desktop apps for expensive pure computations, I limit the degree of parallelism to leave room for other work, and I benchmark every use.

Question My check
CPU-bound? If not, do not use PLINQ
Big enough? Total sequential time well above milliseconds
Pure lambdas? No shared mutable state
Server request path? Avoid
Ordering needed? Add AsOrdered() deliberately

๐Ÿ”— Follow-ups the interviewer may ask

  1. When: Candidate says PLINQ is wrong for I/O Why: Checks async parallel loops
    How to answer

    Parallel.ForEachAsync (.NET 6+) runs an async body for each item in a sequence (IEnumerable<T> or IAsyncEnumerable<T>) with a bounded number of concurrent operations, set by ParallelOptions.MaxDegreeOfParallelism (default: processor count). The body receives a CancellationToken that is cancelled if any iteration throws, so it stops early on failure. It is the simplest way to say "process these 10,000 items, at most 20 at a time". Compared with Task.WhenAll it limits concurrency; compared with SemaphoreSlim it needs less code. It does not return results, so write to a concurrent collection or a preallocated array.

  2. When: Candidate mentions pool threads Why: Checks ThreadPool behaviour
    How to answer

    The ThreadPool is a process-wide set of reusable background threads that run queued work items: Task.Run, async continuations, timer callbacks and ASP.NET Core requests. Work goes into a global queue or, when queued from a pool thread, into that thread's local queue, and idle threads steal from others. The pool starts up to minimum threads (default: processor count) immediately on demand; above that it adds threads gradually, using a hill-climbing algorithm that measures throughput plus a starvation detector that injects threads when queued work is not progressing. That gradual injection is why blocking pool threads causes latency spikes.

๐Ÿ›  Real-world problems: how you handle them

PLINQ helped a batch job but hurt the API
Situation
A [risk platform] calculated scores for [5 million] positions in a nightly job and on demand in an API.
Signals
The job took [100 s]; after adding `AsParallel()` to shared code, API p99 doubled under load.
Investigation
The job ran on an idle machine and gained [6x]; the API's pool threads were already busy serving requests, so PLINQ added contention.
Resolution
Kept PLINQ in the batch job with degree of parallelism core count minus one; removed it from the API path and precomputed scores.
Prevention
Documented that parallel loops are not used in request paths and added load tests.

How to tell it (STAR)

Adding PLINQ to our [risk calculation] made the batch job six times faster but doubled API p99 latency. The API's cores were already busy, so parallelism only added contention. I kept PLINQ for the batch job, removed it from the request path and precomputed scores instead.

โ†ฉ Usually asked after

More in Async Advanced

Last updated 31 Oct 2026.

๐ŸŒ Sangam's Webring

โช Previous ๐Ÿ  Main Next โฉ
๐Ÿ’ฌ Chat with me