ThanhNguyen logo
thanh_nguyen
threading

Understanding Threading and Semaphores in .NET

Understanding Threading and Semaphores in .NET
4 min read
#threading

In software development, improving performance and responsiveness is a constant goal. Whether you're working on a desktop app, web backend, or game engine, knowing how to manage parallel work is essential.

That’s where understanding Threads and Semaphores in .NET becomes crucial.


What is a Thread and a Process?

In .NET — and computer science in general — the terms "thread" and "process" refer to different units of execution:

  • A process is an instance of a program that is running. It contains its own memory space, system resources, and at least one thread.
  • A thread is the smallest unit of execution within a process. Multiple threads can exist within the same process and share memory.

Think of a process as a building, and threads as workers inside that building. They work together but share the same space and tools.


What is the Difference Between Process and Thread?

FeatureProcessThread
MemoryHas its own memory spaceShares memory with other threads
OverheadHigh (creation and switching)Low (lighter and faster)
CommunicationInter-process communication (IPC)Shared memory access
IsolationFully isolated from other processesThreads can affect each other
Use caseRunning separate applicationsParallelism within one app

Why Do We Need Multithreading?

Imagine you're building a web server that needs to handle thousands of requests per minute. If your server processes each request one by one, users will experience serious delays.

Multithreading allows the server to handle multiple requests at the same time, improving responsiveness and user experience.

Other common reasons:

  • Running background tasks (e.g., sending emails, processing files)
  • Improving performance on multicore systems
  • Keeping the UI responsive in desktop or mobile apps

Real-World Scenario: File Conversion Tool

Let’s say you're building a desktop app that converts Word documents to PDF. A user uploads a folder with 100 files.

If you process them one at a time, it could take 10–15 minutes.

Instead, you create multiple threads to convert several files in parallel:

static void ConvertFiles(string[] filePaths)
{
    List<Thread> threads = new();
    
    foreach (var path in filePaths)
    {
        var thread = new Thread(() => ConvertToPdf(path));
        thread.Start();
        threads.Add(thread);
    }

    foreach (var t in threads) t.Join();
}

static void ConvertToPdf(string filePath)
{
    // Simulate conversion
    Console.WriteLine($"Converting: {filePath}");
    Thread.Sleep(2000);
    Console.WriteLine($"Finished: {filePath}");
}

⚠️ This works, but launching 100 threads is overkill and consumes too many system resources. That’s where semaphores come in.


What is a Semaphore?

A Semaphore controls how many threads can access a resource or block of code at once.

It’s like a parking lot: if there are 5 spots and all are full, the 6th car has to wait until someone leaves.

In .NET, we usually use SemaphoreSlim for this purpose.


Real-World Scenario: API Rate Limiting

Suppose you're calling a third-party API that allows only 3 concurrent calls.

You have 20 tasks that need to hit that API — if you send them all at once, the API will reject some of them.

Use a SemaphoreSlim to control access:

static SemaphoreSlim semaphore = new(3); // max 3 concurrent API calls

static async Task CallApiAsync(int taskId)
{
    Console.WriteLine($"Task {taskId} waiting...");
    await semaphore.WaitAsync();

    try
    {
        Console.WriteLine($"Task {taskId} started...");
        await Task.Delay(2000); // Simulate API call
        Console.WriteLine($"Task {taskId} done.");
    }
    finally
    {
        semaphore.Release();
    }
}

static async Task MainAsync()
{
    var tasks = Enumerable.Range(1, 20).Select(CallApiAsync);
    await Task.WhenAll(tasks);
}

With SemaphoreSlim(3), only 3 tasks run concurrently. The others wait and take turns.


When to Use Threads vs Tasks

Use CaseUse
Long-running background workThread
Short asynchronous operationsTask, async/await
Precise control over concurrencyThread, Semaphore
Server/web applicationsTask, thread pool, or parallelism

🧠 Tip: Always prefer Task in modern .NET unless you specifically need low-level thread control.


Advanced Case: Limiting Concurrent File Uploads in ASP.NET Core

In a web app, users can upload large files. You want to limit concurrent uploads to 5 at a time to avoid overloading the server.

public class UploadController : Controller
{
    private static SemaphoreSlim _uploadSemaphore = new(5); // limit to 5 concurrent uploads

    [HttpPost("upload")]
    public async Task<IActionResult> Upload(IFormFile file)
    {
        await _uploadSemaphore.WaitAsync();

        try
        {
            // Simulate saving file
            await Task.Delay(3000);
            return Ok("File uploaded");
        }
        finally
        {
            _uploadSemaphore.Release();
        }
    }
}

Wrapping Up

Multithreading and Semaphores are essential tools in your .NET toolkit.

  • Use threads or tasks to do work in parallel.
  • Use semaphores to avoid overloading resources by controlling concurrency.

When applied correctly, they lead to faster, more responsive, and more scalable applications.

“With great concurrency comes great responsibility.” 🕸️


Learn More


Have you used Semaphores in your own project? What threading challenges have you faced in .NET? Let’s discuss in the comments!