multithreading
foreach loop
parallel processing
C#
threading

Starting a new thread in a foreach loop

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

You can start a new thread inside a foreach loop, but that does not make it a good default pattern. A raw thread per item creates overhead, complicates error handling, and scales badly when the collection grows. In most real C# code, Task, the thread pool, or Parallel.ForEach is the safer abstraction.

The Raw Thread Version

A direct implementation is possible:

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading;
4
5var items = new List<int> { 1, 2, 3, 4 };
6var threads = new List<Thread>();
7
8foreach (var item in items)
9{
10    var localItem = item;
11    var thread = new Thread(() =>
12    {
13        Console.WriteLine($"Processing {localItem} on thread {Thread.CurrentThread.ManagedThreadId}");
14        Thread.Sleep(200);
15    });
16
17    threads.Add(thread);
18    thread.Start();
19}
20
21foreach (var thread in threads)
22{
23    thread.Join();
24}

This works, but notice what you now own yourself: creating each thread, tracking it, waiting for completion, and deciding what to do when something fails. That is a lot of management code for a very common problem.

Why the Loop Variable Still Deserves Attention

The localItem copy makes intent explicit. In modern C#, foreach capture semantics are much less error-prone than they used to be, but closures are still easy to misunderstand when code changes later. If somebody refactors the loop body, introduces mutable shared state, or swaps foreach for another loop shape, the explicit local keeps the behavior obvious.

The bigger issue is not closure capture. It is that a thread-per-item approach has poor cost characteristics. Threads consume memory for their stacks, require OS scheduling, and increase context switching. That cost can dominate the useful work if each iteration is small.

Prefer Tasks for General Concurrent Work

If each iteration is mostly asynchronous or short-lived, Task is a better default than Thread.

csharp
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Threading.Tasks;
5
6var items = new List<int> { 1, 2, 3, 4 };
7
8var tasks = items.Select(async item =>
9{
10    await Task.Delay(200);
11    Console.WriteLine($"Processed {item}");
12});
13
14await Task.WhenAll(tasks);

This gives you better composition and better exception flow. If one task fails, Task.WhenAll can surface that failure in a structured way. You also let the runtime reuse pooled threads instead of forcing a brand-new thread for each item.

Parallel.ForEach for CPU-Bound Work

When each item is independent and the work is CPU-bound, Parallel.ForEach is often a better fit than manually creating threads.

csharp
1using System;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5var items = new List<int> { 1, 2, 3, 4, 5, 6 };
6
7Parallel.ForEach(items, item =>
8{
9    var value = item * item;
10    Console.WriteLine($"{item} -> {value}");
11});

This still runs work concurrently, but it uses the thread pool and internal partitioning logic. That usually gives better throughput and less boilerplate. You can also cap parallelism when needed:

csharp
1Parallel.ForEach(
2    items,
3    new ParallelOptions { MaxDegreeOfParallelism = 2 },
4    item => Console.WriteLine(item));

That control matters when the machine is already busy or when the loop body competes for a limited external resource.

When a New Thread Per Item Is Actually Reasonable

Manual Thread creation is mostly justified when you need thread-specific behavior rather than generic concurrency. Examples include a dedicated long-lived worker, custom apartment state for COM interop, or very specialized thread affinity requirements. Those are uncommon compared with ordinary item processing.

If the loop is handling network I/O, database calls, or file operations, raw threads are especially hard to justify. In those cases, asynchronous APIs usually scale better because they do not tie up a thread while the program is waiting on external work.

Common Pitfalls

The most common mistake is starting hundreds or thousands of threads because the collection is large. That often makes the program slower, not faster.

Another mistake is forgetting that multiple loop iterations may update the same data. Even if thread creation is correct, shared mutable state still needs synchronization with locks, ConcurrentDictionary, channels, or some other safe design.

A third problem is not waiting for completion. If the enclosing method exits before all threads finish, the overall operation may end in a half-complete state.

Summary

  • Starting a raw thread in a foreach loop is possible, but it is usually the wrong abstraction
  • A thread per item adds memory, scheduling, and lifecycle overhead
  • 'Task.WhenAll is a better default for general concurrent or asynchronous work'
  • 'Parallel.ForEach is often the right tool for independent CPU-bound iterations'
  • Use manual Thread creation only when you need thread-level behavior that higher-level APIs cannot express

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track 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.

Browse interview questions

All Rights Reserved.