Tasks and parallel programming
Use every CPU core for heavy calculations: Task.Run, Parallel.For/ForEach, PLINQ, and when parallelism helps or hurts.
I/O-bound vs CPU-bound
| I/O-bound | CPU-bound | |
|---|---|---|
async/await with …Async methods | Task.Run, Parallel, PLINQ | |
Task.Run: move heavy work off the UI thread
private async void AnalyseButton_Click(object sender, RoutedEventArgs e){ AnalyseButton.IsEnabled = false; var report = await Task.Run(() => RunClashDetection(model)); // runs on a pool thread ResultsGrid.ItemsSource = report.Clashes; // back on the UI thread AnalyseButton.IsEnabled = true;}Parallel.For and Parallel.ForEach
double[] utilisation = new double[members.Length]; Parallel.For(0, members.Length, i =>{ utilisation[i] = CheckCapacity(members[i]); // each index written by one thread only: safe}); Parallel.ForEach(drawings, new ParallelOptions { MaxDegreeOfParallelism = 4 }, d =>{ d.Render();});Parallel.For splits the index range into chunks and runs them on several thread-pool threads, then waits until all are done.
PLINQ
var overloaded = members .AsParallel() // the rest of the query runs on many cores .Where(m => CheckCapacity(m) > 1.0) .Select(m => m.Mark) .ToList();When parallel makes things worse
Tiny work per item: the overhead of scheduling is bigger than the work. Summing 1,000 numbers is faster on one thread. Shared state: threads writing the same variable or list need locks, and waiting on locks kills the speed-up (next lesson). Not thread-safe code: List<T>,Dictionaryand many libraries must not be modified from several threads at once. UseConcurrentBag,ConcurrentDictionary, or per-thread results.Order matters: parallel loops finish items in unpredictable order. UI objects: WPF controls may only be touched from the UI thread.
// ✗ data race: many threads doing total += …// ✓ give each thread its own subtotal, combine once at the enddouble total = 0;object gate = new();Parallel.ForEach(members, () => 0.0, // thread-local start value (m, _, local) => local + m.WeightKg, // no sharing inside the loop local => { lock (gate) total += local; }); // combine once per threadConsole.WriteLine(total); Practice
Parallel.For using thread-local counters, and print both results and times with Stopwatch.
Show the solution
using System.Diagnostics; static bool IsPrime(int n){ if (n < 2) return false; for (int d = 2; (long)d * d <= n; d++) if (n % d == 0) return false; return true;} const int N = 2_000_000;var sw = Stopwatch.StartNew();int seq = 0;for (int i = 0; i < N; i++) if (IsPrime(i)) seq++;Console.WriteLine($"Sequential: {seq} in {sw.ElapsedMilliseconds} ms"); sw.Restart();int par = 0;Parallel.For(0, N, () => 0, (i, _, local) => IsPrime(i) ? local + 1 : local, local => Interlocked.Add(ref par, local));Console.WriteLine($"Parallel: {par} in {sw.ElapsedMilliseconds} ms"); Output
Sequential: 148933 in 610 ms Parallel: 148933 in 105 ms
Remember
CPU-bound work benefits from multiple cores; I/O-bound work needs async instead. Task.Runmoves heavy work off the UI thread.Parallel.For/ForEachand PLINQ split work across cores.Avoid shared mutable state; measure the real speed-up.