Concurrency and Multithreading in C#: A Complete Guide for .NET Developers

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Concurrency and Multithreading in C#: from Thread to async/await



Modern applications increasingly require simultaneous execution of multiple tasks: from processing web server requests to parallel computations in desktop programs. In C# (.NET), there is a powerful arsenal of tools for this — from classic threads (Thread) to high-level constructs async/await and the Task Parallel Library (TPL). In this article, we will break down the key concepts of concurrency and multithreading, their practical application, and best practices.



What are concurrency and multithreading?



Before moving to code, it is important to understand the difference between these concepts:



  • Concurrency — the ability of a program to execute multiple tasks by switching between them, creating the illusion of simultaneous execution. This can be implemented even on a single processor core.
  • Multithreading — a form of concurrency where multiple threads execute physically simultaneously on a multi-core processor.
  • Parallelism — a subset of multithreading where different parts of a task are executed strictly simultaneously on different cores.


In C#, you can use both low-level threads and abstractions that automatically manage threads. The choice depends on the task: for I/O-bound operations (working with files, network), async/await is better suited, while for CPU-bound (computations), Parallel.For or Task is used.



Creating and managing threads: the Thread class



The System.Threading.Thread class is the lowest-level way to work with threads in C#. It gives full control but requires careful synchronization.



using System;using System.Threading;

class Program{ static void Main() { Thread thread = new Thread(DoWork); thread.Start();

// Main thread continues working for (int i = 0; i < 5; i++) { Console.WriteLine($"Main thread: {i}"); Thread.Sleep(100); }

thread.Join(); // Wait for the child thread to finish Console.WriteLine("Work completed."); }

static void DoWork() { for (int i = 0; i < 5; i++) { Console.WriteLine($"Child thread: {i}"); Thread.Sleep(150); } }}


Problems with manual thread management:

  • Complexity of synchronization (race conditions, deadlocks).
  • High overhead for creating and destroying threads.
  • Lack of built-in support for returning a result.


Therefore, in modern C#, the Task Parallel Library is preferred.



Task Parallel Library (TPL) and async/await



The Task class from the System.Threading.Tasks namespace represents an asynchronous operation. Combined with the async and await keywords, it allows writing asynchronous code that looks like synchronous code.



Basic example with Task



using System;using System.Threading.Tasks;

class Program{ static async Task Main() { Console.WriteLine("Start of work.");

// Start an asynchronous task Task<int> task = CalculateAsync(10);

// While the task is running, we can do something else Console.WriteLine("Doing other work...");

// Await the result int result = await task; Console.WriteLine($"Result: {result}"); }

static async Task<int> CalculateAsync(int number) { await Task.Delay(1000); // Simulate a long-running operation return number * number; }}

Blogs

Book Recommendations