1. Introduction
Multithreading is like several coworkers working in parallel in an office: one prints documents, another calls a client, a third makes coffee (and, of course, they're all programmers). If we had only one worker, they'd do everything one by one, and the office would choke on boredom and a coffee line. In programming it's similar: a single-threaded program can do only one task at a time.
Imagine our app is doing a long-running operation, for example downloading a file from the internet or computing a huge table. Everything else at that moment is "frozen" — buttons don't respond, animations stop, phrases like "not responding" pop up in a dialog.
Multithreading lets the app do multiple things at once: the UI stays responsive, operations run in parallel, and we don't spiral into a monologue like "Computer, are you frozen again?!".
Basic concepts and terminology
Before diving in, let's figure out what a thread is and how it differs from a process.
- Process (Process): A standalone program with its own address space, variables, resources. For example, each running application in Windows is a separate process.
- Thread (Thread): A unit of execution inside a process. A process can contain one or more threads that share the same resources (memory, variables).
Why does multithreading raise so many questions?
Because threads are wild: they can start running at any moment, mix up data, interrupt each other and generally make a mess in memory if you don't control the order. If it seems like a daycare without a teacher — that's spot on! Discipline and care around threads are the foundation of writing reliable multithreaded programs.
2. History and role of multithreading in C# and .NET
Back in the day C# was single-threaded and programs were simple. As performance demands grew, multi-core CPUs appeared and the need to build responsive apps without UI freezes emerged, .NET got multithreading tools. First came the classic System.Threading.Thread, later Tasks (Task) showed up, async methods (async/await), parallel data processing (PLINQ) and higher-level synchronization primitives.
C# has grown into a powerful platform where multithreading is not exotic but everyday practice.
Visually: process and threads
Here's a simple diagram:
+--------------------------------------------------+
| Process (your program) |
| +-------------+ +-------------+ |
| | Thread 1 | | Thread 2 | |
| +-------------+ +-------------+ |
| ... |
| +-------------+ |
| | Thread N | |
| +-------------+ |
+--------------------------------------------------+
All threads inside a process see shared variables and resources.
3. How to create a thread in C#?
Let's start with the most basic: the Thread class from the System.Threading namespace.
Example: Starting a second thread
Suppose we have a long-running task — for example, summing numbers from 1 to 10_000_000. While the sum runs, the main thread will print a greeting to the user.
using System;
using System.Threading;
class Program
{
// Method for the second task
static void CalculateSum()
{
long sum = 0;
for (int i = 1; i <= 10_000_000; i++)
sum += i;
Console.WriteLine($"[Thread 2] Sum: {sum}");
}
static void Main()
{
// Create a thread, specify a delegate to the method
Thread thread = new Thread(CalculateSum);
thread.Start(); // Start the second thread
// The main thread keeps working
Console.WriteLine("[Thread 1] Hi! We're working in parallel...");
// Wait for the second thread to finish before exiting
thread.Join();
Console.WriteLine("[Thread 1] All done!");
}
}
What will happen?
The screen will show the line "[Thread 1] Hi! We're working in parallel...", and then, when the calculating thread finishes, it will print the sum.
Typical problem: First come, first printed
Try running this code several times — the order of the output lines can vary! Sometimes the sum appears first, sometimes the greeting. That's real multithreading — programs become less predictable, like a cat's mood on Mondays.
4. Memory zone: what do threads see?
All threads inside a process have access to the same variables (unless they're local to a method). If you change a variable in one thread — others will see it!
Example: Shared variable
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 1000; i++)
counter++;
}
static void Main()
{
Thread t1 = new Thread(Increment);
Thread t2 = new Thread(Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"counter = {counter}");
}
}
What do we expect to see in counter? Logic suggests 2000, since each thread increments 1000 times.
But no! Run it multiple times — you'll see different values: 1782, 1935, 1999…
Why? This is the classic Race Condition — threads "steal" execution from each other between reading -> incrementing -> writing, and some increments get lost.
5. Useful nuances
How do threads interact with the UI?
In modern desktop apps (WinForms/WPF/MAUI) the main thread handles the graphical UI. All user actions (clicks, input) run on that thread. Background tasks should run on other threads, but it's rule-of-thumb that you can't directly "touch" the UI from other threads. That's to prevent chaos.
In console apps you don't have that restriction; you can call Console.WriteLine from any thread. However, in real apps without proper synchronization the UI can "go wrong".
Sequencing and parallelism
Table — to reinforce differences.
| Single-threaded code | Multithreaded code |
|---|---|
| Executes tasks sequentially | Tasks can run simultaneously |
| UI "freezes" during long tasks | UI stays responsive |
| Easy to read and write variables | Requires control over data access |
| Easy to debug | Can be hard to debug |
Important points when working with threads
- Shared variables — shared risk. As shown above, if multiple threads use a shared variable — without synchronization errors can happen! (More in following lectures.)
- You can "wait" for a thread via Join(). The Join() method lets the main thread "pause" until a background one finishes. Use it when you need to wait for a result.
- A thread can't be started twice. After a thread has finished you can't start it again — you have to create a new Thread object.
- Thread termination. A thread ends when its method finishes. Forcibly "killing" threads is a bad idea (the Abort() method is long considered harmful and obsolete).
What is multithreading used for in practice?
- UI apps: to avoid freezing the interface while background loading or computations run.
- Servers and services: to handle requests from many clients at the same time.
- High-performance computing: split a large job (e.g., processing millions of records) into parts and run them in parallel.
- Games, simulations, data processing: to model complex systems without losing performance.
Problems of multithreading
Multithreading gives you power, but adds complexity:
- Race Condition (sostoyanie gonki): when multiple threads change the same data at the same time, the result depends on instruction ordering, which is unpredictable.
- Deadlock (vzaimnaya blokirovka): threads wait for each other and nobody can proceed.
- Starvation (golodanie): one thread is constantly deprived of access to a resource.
In this lecture we only mentioned the main multithreading problems; in the next ones we'll learn to recognize and avoid them. For now — just remember that if everything "froze", it might not only be bugs, but threads that "threw a party".
GO TO FULL VERSION