C# High Performance Computing

High performance computing (HPC) makes a program faster by using more of the machine: several cores for heavy math (parallel processing), overlapping slow I/O with other work (concurrency), or separate, isolated processes (multiprocessing).

Every modern computer is a parallel machine — even a phone has several cores. This chapter teaches the three tools .NET gives you to use them, and the pitfalls behind each one.

Parallel Processing

Parallel processing splits one big CPU-bound job — such as scanning a large collection — across several cores that run at the same time. The Task Parallel Library (TPL) manages the worker threads for you; in modern code you never create raw threads yourself.

parallel processing across cores

Parallel processing — one job split across cores

Task Parallel Library (TPL)

Parallel.For distributes loop iterations across threads of the thread pool. Use it only when every iteration is independent.

// Parallel.For splits the loop across worker threads automatically
Parallel.For(0, 10, i =>
{
    // each iteration runs on whatever thread the pool assigns
    Console.WriteLine($"item {i} on thread {Thread.CurrentThread.ManagedThreadId}");
});
// note: output order is NOT guaranteed — iterations finish in any order

PLINQ

LINQ gets parallelism almost for free with AsParallel(): the query partitions the source, processes chunks on several cores, then merges the results back.

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

// AsParallel() lets the LINQ pipeline use all cores at once
var firstPrimes = numbers.AsParallel()
                         .Where(IsPrime)   // runs on several threads in parallel
                         .Take(100)        // stop early once enough primes found
                         .ToArray();

static bool IsPrime(int n)
{
    if (n < 2) return false;
    for (int d = 2; d * d <= n; d++)
        if (n % d == 0) return false;  // found a divisor — not prime
    return true;
}

Concurrency with async/await

Concurrency is about overlapping: while the program waits for a slow network or disk response, the rest of the work keeps moving. async/await keeps the calling thread free instead of blocking it.

concurrency overlapping tasks

Concurrency — tasks overlap while waiting

async / await

An async method returns a Task and yields control at each await. No thread is parked on the network, and the continuation runs when the result is ready.

static async Task<string> FetchAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);   // yields; caller keeps running meanwhile
}

// await restores the result when it is ready
string html = await FetchAsync("https://learn.microsoft.com");
Console.WriteLine($"downloaded {html.Length} characters");

Running Tasks Together

Task.WhenAll starts several tasks at once and waits for all of them; WhenAny waits for the first to finish. This is how you overlap independent I/O.

Task<string> docs   = FetchAsync("https://learn.microsoft.com");
Task<string> dotnet = FetchAsync("https://dotnet.microsoft.com");
// both requests are now in flight at the same time

string[] results = await Task.WhenAll(docs, dotnet);   // waits for both
Console.WriteLine(results.Length);                      // 2

Multiprocessing

A process is an isolated program with its own memory and its own .NET runtime. Processes cannot read each other's memory by accident — that isolation is exactly what you want for untrusted or crash-prone work.

multiprocessing isolated processes

Multiprocessing — isolated processes

Starting Processes

Process.Start launches an external program. Redirect its output to capture the result, and set UseShellExecute = false so the child stays attached to your console app.

using System.Diagnostics;

var startInfo = new ProcessStartInfo("dotnet", "--version")
{
    RedirectStandardOutput = true,   // capture the child's stdout here
    UseShellExecute = false          // no shell wrapper — talk to the process directly
};

using var proc = Process.Start(startInfo)!;   // launches a separate OS process
string version = await proc.StandardOutput.ReadToEndAsync();
Console.WriteLine(version);                   // e.g. "9.0.100"

Inter-Process Communication

Processes share only what they are explicitly told to share. The simplest channels are redirected standard input/output (used above), files, named pipes (System.IO.Pipes), or a local HTTP server. Keep the payload small and the format versioned.

Pitfalls & Choosing a Model

Race Conditions

Parallel code that touches shared state breaks silently: two threads can read and write the same variable at the same instant, losing updates.

int counter = 0;
Parallel.For(0, 100_000, _ => counter++);   // BUG: racing increments lose updates
Console.WriteLine(counter);                   // often far less than 100_000

Protect the shared state with a lock — only one thread may enter the block at a time, so the result becomes deterministic.

int safe = 0;
object gate = new();   // any object can act as a lock

Parallel.For(0, 100_000, _ =>
{
    lock (gate) safe++;   // only one thread inside at a time
});

Console.WriteLine(safe);  // exactly 100_000

Which Model When

The three models solve different problems. Match the tool to the bottleneck, never to fashion.

ScenarioModel.NET tool
CPU-heavy loop over independent dataParallel processingParallel.For, PLINQ
Slow I/O, or keeping a UI responsiveConcurrencyasync/await, Task.WhenAll
Untrusted or crash-prone work, external toolsMultiprocessingProcess.Start, pipes
Shared counters or stateProtect, do not parallelize blindlylock, Interlocked