Skip to Content
Course content

140: Practice Exercise: Building a Rate Limiter with SemaphoreSlim

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a small, boutique coffee shop. You've got a great espresso machine, but you only have three small tables. If ten people walk in wanting to sit and work on their laptops, you can't just magically create more space. Only three people get a seat; the other seven have to wait in a line. As soon as one person finishes their latte and leaves, the next person in line steps up and takes that seat.

In the world of C#, SemaphoreSlim is essentially your coffee shop manager. It doesn't stop the work from happening entirely, but it limits how many things happen at the exact same time. This is critical when you're dealing with external APIs that will ban your IP address if you hit them with 500 concurrent requests, or a database that starts choking when too many connections open at once.

Mapping the Seats to the Code

Let's look at how that coffee shop analogy translates directly into a rate limiter:

  • The Tables: This is the initialCount you pass into the SemaphoreSlim constructor. If you want a limit of 3 concurrent requests, you initialize it with 3.
  • The Line: When a task calls WaitAsync(), it's like a customer joining the queue. If a "seat" is available, they go right in. If not, the task pauses (without blocking the thread) until someone else leaves.
  • Drinking Coffee: This is your actual logic—the HTTP request or the heavy file processing.
  • Leaving the Shop: This is the Release() method. It tells the semaphore, "I'm done, someone else can take my spot now."

Controlling the Flood

Here is a pattern I use all the time. The key is ensuring that Release() is called no matter what happens—even if the API call throws an exception. If you forget to release the semaphore, you've effectively "deleted a table" from your shop. Do that three times, and your entire application deadlocks because the line will never move.

public class ApiRateLimiter
{
    // We only allow 3 concurrent requests to the external service
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(3, 3);

    public async Task<string> ExecuteRequestAsync(int requestId)
    {
        Console.WriteLine($"Request {requestId} is waiting for a slot...");
        
        // This is where the "line" forms
        await _semaphore.WaitAsync();

        try
        {
            Console.WriteLine($"Request {requestId} has entered. Processing...");
            // Simulate an API call
            await Task.Delay(new Random().Next(1000, 3000)); 
            return $"Response from request {requestId}";
        }
        finally
        {
            // IMPORTANT: Always release in a finally block
            Console.WriteLine($"Request {requestId} is leaving. Slot freed.");
            _semaphore.Release();
        }
    }
}

Why not just use a Lock?

You might be wondering why we don't just use a lock statement. A lock is like a single-person bathroom; only one person can be in there at a time. That's too restrictive for most high-performance scenarios. SemaphoreSlim is lightweight and, more importantly, it supports WaitAsync(). This means your threads aren't sitting there idling (and wasting memory) while waiting for a slot to open up; they are returned to the thread pool to do other useful work.




📋 Practical Task

Exercise: Build a Concurrent Image Downloader

You are building a tool that downloads 20 high-resolution images from a server. However, the server has a strict policy: if you attempt more than 4 concurrent downloads, it will trigger a 429 (Too Many Requests) error and block you.

Your Task: Create a class called ImageDownloader that implements the following:

  • A SemaphoreSlim configured to allow a maximum of 4 concurrent operations.
  • A method DownloadImageAsync(int imageId) that simulates a download using await Task.Delay() (use a random delay between 500ms and 2000ms).
  • Ensure the semaphore is released correctly using a try...finally block.
  • In your Main method, start 20 download tasks simultaneously using Task.WhenAll to prove that only 4 downloads are ever running at once.

Expected Output: Your console should show a pattern where 4 images start downloading, and as soon as one finishes, exactly one more begins, until all 20 are complete.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.