-
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
57: Practice Exercise: Building a Concurrent File Downloader
How do I start a bunch of downloads without just doing them one by one?
If you use a standard foreach loop and await the download call inside it, you're not actually doing concurrent work. You're just doing sequential work with extra steps. To get true concurrency, you want to create a collection of Task objects first, and then tell C# to wait for all of them to finish.
I usually recommend Task.WhenAll for this. It's clean and it's efficient. Here is the pattern I use:
var downloadTasks = urls.Select(url => DownloadFileAsync(url));
await Task.WhenAll(downloadTasks);
Notice that I'm not await-ing DownloadFileAsync inside the Select. I'm just calling the method, which returns a Task (a promise of future work), and sticking that task into a list. The actual waiting happens only once at the end.
If I have 100 files to download, should I just fire off 100 tasks at once?
Honestly? No. Please don't do that. If you try to open 100 simultaneous TCP connections, you're likely to either get rate-limited by the server (who will think you're performing a DDoS attack) or you'll exhaust your own machine's socket pool. It's a classic rookie mistake that works fine with three files but crashes in production with three thousand.
The professional way to handle this is by using a SemaphoreSlim. Think of it as a nightclub bouncer; it only lets a specific number of tasks "into the club" at one time. Here's how I'd implement a throttle:
private readonly SemaphoreSlim _bouncer = new SemaphoreSlim(4); // Only 4 at a time
async Task DownloadFileAsync(string url)
{
await _bouncer.WaitAsync();
try
{
// Actual HttpClient logic here
var data = await _httpClient.GetByteArrayAsync(url);
await File.WriteAllBytesAsync(GetFileName(url), data);
}
finally
{
_bouncer.Release(); // Let the next task in
}
}
What happens if one download fails? Does the whole process crash?
This is where Task.WhenAll can be a bit tricky. If one of the tasks throws an exception, Task.WhenAll will eventually throw that exception. But here's the catch: the other downloads are still running in the background. They don't just stop because one failed.
If you want your downloader to be resilient, you shouldn't let the exception bubble up to the WhenAll call. Instead, wrap the logic inside the task in a try-catch block. I usually return a small result object so I can report exactly which files failed at the end without killing the entire process.
public record DownloadResult(string Url, bool Success, string Error = "");
async Task<DownloadResult> DownloadFileAsync(string url)
{
try
{
await _httpClient.GetAsync(url);
return new DownloadResult(url, true);
}
catch (Exception ex)
{
return new DownloadResult(url, false, ex.Message);
}
}
By doing this, Task.WhenAll returns an array of DownloadResult objects. You can then simply filter that list to see who failed and maybe retry just those specific ones.
📋 Practical Task
Exercise: The High-Res Wallpapers Batch Downloader
Your goal is to build a console application that downloads a list of high-resolution images from a set of URLs. To make this a real-world scenario, you must implement the following constraints:
- The Source: Create a
List<string>containing 10-15 URLs of images (you can use placeholder image services likepicsum.photos). - The Throttle: Use a
SemaphoreSlimto ensure that no more than 3 downloads are happening simultaneously. - The Resilience: Wrap each download in a try-catch block. If a download fails, the program should not crash; instead, it should log the failed URL to the console and continue with the remaining files.
- The Completion: Use
Task.WhenAllto wait for all downloads to conclude. Once finished, print a summary showing the total number of successful downloads and the total number of failures.
Bonus: Implement a simple timeout using a CancellationTokenSource so that if a single image takes longer than 10 seconds to download, that specific task is cancelled.
There are no comments for now.