Does Parallel.ForEach limit the number of active threads?
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
Yes, Parallel.ForEach limits the number of active threads. It uses the .NET ThreadPool and an internal partitioner that dynamically adjusts concurrency based on system load, available cores, and work item duration. By default, it does not spin up one thread per element — it starts conservatively and scales up or down. You can explicitly set a maximum degree of parallelism using ParallelOptions.MaxDegreeOfParallelism to cap the number of concurrent operations.
Default Behavior
By default, Parallel.ForEach uses the ThreadPool's hill-climbing algorithm to determine how many threads to use. It starts with a small number (typically equal to the processor count) and adds or removes threads based on throughput measurements. For CPU-bound work, it generally converges to one thread per core.
Setting MaxDegreeOfParallelism
MaxDegreeOfParallelism sets an upper bound on concurrent executions. The runtime may still use fewer threads if it determines that is optimal. Setting it to -1 (the default) means no explicit limit — the runtime decides.
When to Limit Parallelism
I/O-bound work (database, HTTP, file access)
Without a limit, Parallel.ForEach on I/O-bound work can spin up dozens of threads (because the ThreadPool detects threads are idle during I/O waits), overwhelming the database server or exhausting the connection pool.
Rate-limited APIs
Cancellation Support
ParallelLoopState.Stop() prevents new iterations from starting but lets in-progress iterations finish. ParallelLoopState.Break() stops after all iterations with indices lower than the current one complete.
Parallel.ForEach vs Task.WhenAll
For async/I/O-bound operations, Task.WhenAll with a semaphore is preferred over Parallel.ForEach because it does not block threads while waiting for I/O.
How the ThreadPool Scales
The ThreadPool starts with Environment.ProcessorCount threads. If threads are blocked (e.g., by Thread.Sleep or I/O), the pool injects new threads every 500ms-1s. This slow injection rate is why Parallel.ForEach appears to gradually increase parallelism for blocking operations.
Parallel.ForEachAsync (.NET 6+)
.NET 6 introduced Parallel.ForEachAsync for async workloads:
This is the recommended approach for I/O-bound parallel work in modern .NET. It respects MaxDegreeOfParallelism and does not block threads during async waits.
Common Pitfalls
- Not setting
MaxDegreeOfParallelismfor I/O work: The ThreadPool keeps injecting threads when existing ones block on I/O, potentially creating hundreds of threads that overwhelm external services. Always set an explicit limit for I/O-bound operations. - Using
Parallel.ForEachfor async work:Parallel.ForEachexpects synchronous delegates. Calling.Resultor.Wait()inside it blocks ThreadPool threads and can cause deadlocks. UseParallel.ForEachAsyncorTask.WhenAllwith a semaphore instead. - Assuming one thread per item:
Parallel.ForEachpartitions the collection and reuses threads across partitions. The number of threads is typically far less than the number of items. - Shared mutable state without locking:
Parallel.ForEachexecutes delegates on multiple threads simultaneously. Accessing shared variables (counters, lists, dictionaries) withoutlock,Interlocked, orConcurrentDictionarycauses race conditions. - Setting
MaxDegreeOfParallelism = 1for debugging: While this serializes execution for debugging, it changes behavior (no parallel exceptions, different timing). Use conditional compilation or a debugger conditional instead.
Summary
Parallel.ForEachdynamically limits threads using the ThreadPool's hill-climbing algorithm- Use
ParallelOptions.MaxDegreeOfParallelismto set an explicit upper bound on concurrency - For CPU-bound work, the default (processor count) is usually optimal
- For I/O-bound work, always set an explicit limit to avoid thread explosion
- Use
Parallel.ForEachAsync(.NET 6+) for async operations instead of blocking insideParallel.ForEach
Related reading
- Does PHP have threading?
- Does python-requests support HTTP2 and asynchronous calls?
- Does Python support multithreading? Can it speed up execution time?
- Does react-native support Multithreading and Background threading or Parallel Execution? How can we do that?
- Does String.GetHashCode consider the full string or only part of it?
- Does Task.ContinueWith capture the calling thread context for continuation?
- Does ruby have real multithreading?
- Does Spring publish beans in thread-safe manner?

OOD Fundamentals
Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.