🔷 C#

Multithreading e Assincronia em C# (.NET): Um Guia Detalhado

What is it and why? (a real-life analogy)

Imagine you are a chef in a kitchen. You have one gas stove with one burner. If you are cooking soup, while it is boiling, you stand and wait. You cannot chop a salad or fry cutlets at the same time. This is synchronous work. A program works exactly the same way: it executes one command after another, and while, for example, a file is being downloaded from the internet, the entire program interface "freezes" and waits.

Now imagine you have a second burner. You put the soup on the first one, and on the second one you start frying eggs. You watch both burners, switching between them. This is multithreading. Each burner is a separate thread of execution. A program can do several things simultaneously: in one thread, load data; in another, draw animation on the screen.

Now imagine you have one burner again, but you are a very smart chef. You put the soup on to boil, and instead of standing and staring at the pot, you step away to chop a salad. When the soup boils, you hear a whistle and return to it. You are not doing two things at once, but you are not wasting time waiting. This is asynchrony. The program does not block while waiting, but goes off to do other work, and then returns when the result is ready.

Important: Multithreading is about parallel execution (truly simultaneous if the processor has multiple cores). Asynchrony is about efficiently using a single thread without downtime. In C#, they often work together, but they are different concepts.

Why is this needed in programming? Imagine your application needs to:

  • Download 100 photos from the internet
  • Process each one (resize, apply a filter)
  • Show the result to the user

If you do this synchronously, the user will sit and stare at an empty screen for 5 minutes. The application will "freeze". With multithreading, we can download 4 photos simultaneously (according to the number of processor cores). With asynchrony, we can start the download and not block the interface — the user can browse already loaded photos while the rest are still loading.

Theory: how it works internally

Processes and Threads

When you launch a program, the operating system creates a process for it — this is an isolated environment with its own memory. Inside the process, threads can run. A thread is the smallest unit of execution to which the operating system allocates processor time.

Each thread has:

  • Call stack — memory for local variables and method calls
  • Registers — the current execution state
  • Priority — how often the thread gets processor time

If the processor has 4 cores, it can execute up to 4 threads truly simultaneously. If there are more threads than cores, the operating system creates the illusion of parallelism by quickly switching between threads (this is called preemptive multitasking).

Threads in .NET

In .NET, there are two levels of working with threads:

  • Managed threads — threads managed by the CLR (Common Language Runtime) environment. This is what we work with as developers.
  • OS threads — operating system threads. Each managed thread usually corresponds to one OS thread.

Creating a new thread is an expensive operation (takes about 1 microsecond and requires about 1 MB of memory for the stack). Therefore, .NET has a ThreadPool — a set of pre-created threads that can be reused.

Asynchrony and Task

Asynchrony in C# is built on the concept of a Task. A Task is a promise to perform work in the future. When you call an asynchronous method, it returns a Task immediately, while the actual work may be performed in another thread or without any thread at all (for example, waiting for a response from a server does not require a thread at all).

Key keywords:

  • async — a marker for a method that can use await
  • await — suspends the execution of the current method until the Task completes, but does not block the thread
How it works under the hood: When the compiler sees an async method, it turns it into a state machine. The method is split into parts: before the first await, between awaits, after. When execution reaches await, the method "remembers" its state and returns control to the calling code. When the Task completes, the method continues from the same place.

Synchronization and Race Conditions

When multiple threads access the same data simultaneously, a race condition occurs. For example, two threads simultaneously increment a counter by 1. The "read, increment, write" operation is not atomic — one thread may read the value, the second will also read the same value, both will increment and write — as a result, the counter will increase by 1, not by 2.

To protect data, the following are used:

  • lock — a lock that allows only one thread through
  • Monitor, Mutex, Semaphore — more complex synchronization primitives
  • Interlocked — atomic operations for simple cases
  • ConcurrentCollection — thread-safe collections

Syntax in C# (.NET): all forms and variants

1. Thread class (low-level work)

// Creating and starting a thread
Thread thread = new Thread(() => 
{
    Console.