-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
140: Practice Exercise: Building a Rate Limiter with SemaphoreSlim
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
initialCountyou pass into theSemaphoreSlimconstructor. 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
SemaphoreSlimconfigured to allow a maximum of 4 concurrent operations. - A method
DownloadImageAsync(int imageId)that simulates a download usingawait Task.Delay()(use a random delay between 500ms and 2000ms). - Ensure the semaphore is released correctly using a
try...finallyblock. - In your
Mainmethod, start 20 download tasks simultaneously usingTask.WhenAllto 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.
There are no comments for now.