Concurrency in C#: How to Keep Your Program from Getting Bored
\n\nImagine you are a head chef. You have three burners, an oven, and a mixer. If you do everything sequentially — first chop the onions, then fry, then whip the cream — dinner will take forever. It is much more efficient to turn on the oven while you chop vegetables, and whip the cream while the meat is stewing. This is concurrency — the ability to execute several tasks "simultaneously" by switching between them so that resources are not idle.
\n\nIn C#, there are three main tools for this: classic threads (Thread), modern tasks (Task) with async/await, and the concept of goroutines implemented through iterators. In this article, we will break down each approach, compare their performance, and show when to use what. If you write in C# and want your code to fly rather than crawl, you have come to the right place.
\n\nThreads: Heavy Artillery
\n\nA thread is the lowest-level way to organize concurrency. Each thread is a separate "lightweight process" inside your program that has its own stack and context. Creating a thread in C# is very simple:
\n\nusing System;\nusing System.Threading;\n\nclass Program\n{\n static void Main()\n {\n Thread thread = new Thread(DoWork);\n thread.Start();\n Console.WriteLine("Main thread is working");\n thread.Join(); // wait for completion\n }\n\n static void DoWork()\n {\n for (int i = 0; i < 5; i++)\n {\n Console.WriteLine($"Thread: {i}");\n Thread.Sleep(200);\n }\n }\n}\n\nHere we created a separate thread that counts to five, while the main thread outputs a message. Without Join, the program could finish before it reaches five.
Disadvantages of Threads
\n- \n
- Heavy weight. Each thread reserves about 1 MB of memory for the stack. Creating 1000 threads means occupying 1 GB just like that. \n
- Complex synchronization. To prevent two threads from corrupting shared data, you need locks, mutexes, and semaphores. One mistake — and you get a deadlock or race condition. \n
- Context switching. The operating system spends time switching between threads. If there are more threads than CPU cores, this becomes a bottleneck. \n
Threads are good when you need true parallelism on a multi-core processor for heavy computations. But for I/O (working with files, network, database), they are overkill.
\n\nTask and async/await: The Modern Standard
\n\nStarting with .NET 4.5, Microsoft offered an elegant alternative — the Task Parallel Library (TPL) and the async/await keywords. These are not threads in the pure sense, but promises: you tell the compiler "start this work, and when it finishes — return the result." While the work is being executed, the thread is not blocked but returned to the pool.
using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nclass Program\n{\n static async Task Main()\n {\n Console.WriteLine("Starting download...");\n string data = await DownloadDataAsync();\n Console.WriteLine($"Downloaded: {data.Length} characters");\n }\n\n static async Task<string> DownloadDataAsync()\n {\n using HttpClient client = new HttpClient();\n string result = await client.GetStringAsync("https://example.com");\n return result;\n }\n}\n\nNote: await does not block the thread. While data is being downloaded over the network, the thread returns to the pool and can serve other requests. This is ideal for web servers and GUI applications — the interface does not freeze.
How does it work under the hood?
\nWhen the compiler sees async, it turns the method into a state machine. At each await, the method "remembers" its state