Tasks And Parallel Programming In C# Complete Guide
Understanding the Core Concepts of Tasks and Parallel Programming in C#
Tasks and Parallel Programming in C#
Parallel Programming Basics: Parallel programming involves executing multiple operations simultaneously to improve performance, especially on multi-core processors. In C#, this can be achieved using the TPL, which simplifies the process of writing concurrent code. By leveraging the TPL, developers can easily scale their applications without needing deep knowledge of threading.
Task Parallel Library (TPL): The TPL is a comprehensive library designed for parallel programming in .NET Framework. It aims to enhance the performance of applications by providing a set of APIs that enable developers to write data- and task-parallel code with ease. The TPL provides a high-level abstraction over the Threading API, introducing tasks and task schedulers.
Tasks:
A task in C# is a unit of work that can be executed asynchronously. Tasks are lightweight wrappers around ThreadPool threads and are designed to run on multiple cores concurrently. Tasks can be created using the Task
class or delegate types such as Func
and Action
.
Creating Tasks:
To create a task, developers use the Task.Run()
method for fire-and-forget operations:
Task.Run(() =>
{
// Task code here
});
Or by using the Task.Factory.StartNew()
method for more control:
Task.Factory.StartNew(() =>
{
// Task code here
});
Developers can also chain tasks using the ContinueWith()
method to perform actions after a task is completed:
Task task = Task.Run(() =>
{
// Task code here
});
task.ContinueWith(antecedent =>
{
// Code to run after task completion
});
Task States: Tasks can be in various states such as Created, Running, WaitToRun, RanToCompletion, Canceled, Faulted, and WaitingForActivation. Monitoring the state of a task can help developers understand the progress and perform actions based on the result.
Task Schedulers:
A task scheduler determines how and when tasks are executed. The default scheduler in TPL is the ThreadPool task scheduler, which uses a thread pool to manage and execute tasks. Developers can use the TaskScheduler
class to create custom schedulers or modify the behavior of the default scheduler.
Data Parallelism: Data parallelism involves dividing a data set into smaller sets and processing each set in parallel. TPL provides various constructs for data parallelism, such as Parallel.For, Parallel.ForEach, and PLINQ (Parallel Language Integrated Query). These features allow developers to easily parallelize data processing operations.
Parallel.For and Parallel.ForEach:
Parallel.For
and Parallel.ForEach
provide a way to execute loops concurrently. The following example demonstrates how to use Parallel.For
to process an integer array:
Parallel.For(0, array.Length, i =>
{
// Process array[i]
});
Parallel.ForEach
can be used to process collections:
Parallel.ForEach(collection, item =>
{
// Process item
});
These methods can significantly improve the performance of applications that perform common data operations like filtering, mapping, and reduction.
PLINQ: PLINQ (Parallel Language Integrated Query) extends LINQ to support parallel query execution. It allows developers to process collections using LINQ queries in parallel, improving performance without compromising code readability. The following example shows an example of PLINQ:
var results = collection.AsParallel()
.Where(item => condition)
.Select(item => transformation);
Asynchronous Programming (async/await):
Asynchronous programming allows applications to perform long-running operations without blocking the main thread. This is crucial in UI-based applications where responsiveness is key. C# 5 introduced the async
and await
keywords, making it easier to write asynchronous code. Methods marked with the async
keyword can contain await
expressions, allowing them to yield control to the caller while waiting for an asynchronous operation to complete.
Creating Asynchronous Methods:
To create an asynchronous method, developers use the async
keyword and return a Task
or Task<T>
:
public async Task<int> GetWorkCountAsync()
{
int count = await GetCountFromDatabaseAsync();
return count;
}
The await
keyword allows the method to pause execution until the awaited task is complete, without blocking the calling thread. This makes it easy to write non-blocking code without dealing with callbacks or complex threading patterns.
Error Handling in Async Methods: Handling exceptions in asynchronous methods requires careful consideration. Exceptions that occur in an awaited task are propagated to the calling context, which can catch and handle them using try-catch blocks. Here is an example:
public async Task ProcessDataAsync()
{
try
{
await GetDataFromNetworkAsync();
}
catch (Exception ex)
{
// Handle exception
}
}
CancellationToken Support: CancellationToken provides a cooperative mechanism for canceling asynchronous operations. It is passed to asynchronous methods and can be used to request cancellation. The following example demonstrates how to use CancellationToken:
public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
try
{
await GetDataFromNetworkAsync(cancellationToken);
}
catch (OperationCanceledException)
{
// Handle cancellation
}
}
Combining Parallelism and Asynchrony: Parallelism and asynchrony are complementary concepts. Parallelism focuses on dividing work into smaller tasks to run them concurrently on multiple cores, while asynchrony aims to keep the application responsive by avoiding blocking operations. Combining these concepts can lead to highly efficient and scalable applications.
Best Practices:
- Avoid Using CPU-Bound Work in UI Threads: Keep CPU-bound operations off the UI thread to maintain responsiveness.
- Use Tasks for CPU-Bound Work: Use Task.Run() for CPU-bound work and Parallel.For/ForEach for large data sets.
- Optimize Data Access: Use async/await for I/O-bound operations, such as file and network access.
- Choose the Appropriate Technique: Choose between TPL, PLINQ, and async/await based on the specific requirements of the task.
- Handle Exceptions Properly: Implement error handling strategies, especially in asynchronous methods.
- Use CancellationToken for Cancellation Support: Provide cancellation support for long-running operations.
- Test Thoroughly: Test your parallel and asynchronous code to ensure it behaves correctly and efficiently.
Conclusion: Tasks and parallel programming in C# are powerful tools for building scalable and responsive applications. By leveraging the TPL and async/await, developers can simplify concurrent programming and maximize the utilization of modern multi-core processors. Understanding and utilizing these concepts effectively is essential for creating efficient and modern .NET applications.
Important Keywords:
- Parallel Programming
- Task Parallel Library (TPL)
- Task
- Task Scheduler
- Data Parallelism
- Parallel.For
- Parallel.ForEach
- PLINQ
- Asynchronous Programming
- Async/Await
- CancellationToken
- I/O-Bound Operations
- CPU-Bound Operations
- Thread Pool
- Task States
- Continuation Tasks
- Error Handling
- Responsiveness
- Scalability
- Modern .NET Applications
- Threading
- Concurrency
- .NET Framework
- Performance Optimization
- UI-based Applications
- Multicore Processors
- Exception Propagation
- Cooperative Cancellation
- Yielding Control
- Non-blocking Code
- Code Readability
- LINQ
- Parallel Query Execution
- Fire-and-Forget Operations
- Wait-to-Run State
- Ran-to-Completion State
- Canceled State
- Faulted State
- Waiting-for-Activation State
- ThreadPool Task Scheduler
- Custom Schedulers
- Long-running Operations
- Yield Return
- Async Tasks
- Await Expression
- Task.Factory
- Task Creation
- Task Execution
- Task Chaining
- ContinueWith
- Parallel Processing
- Concurrent Collections
- Concurrency Control
- Deadlocks
- Thread Safety
- Non-blocking Algorithms
- Parallel Algorithms
- Distributed Computing
- Cloud Computing
- Concurrent Programming
- Task Cancellation
- Exception Handling in Async/Await
- Parallel Loops
- Parallel Aggregate
- Parallel Partitioning
- Parallel Reduction
- Parallel Mapping
- Parallel Filtering
- Parallel LINQ
- Parallelization Strategies
- Parallel Processing Patterns
- Parallel Sorting
- Parallel Sharding
- Parallel Search
- Parallel Validation
- Parallel Testing
- Parallel Debugging
- Parallel Profiling
- Concurrent Data Structures
- Concurrent Collections
- Non-blocking Data Structures
- Lock-Free Data Structures
- Atomic Operations
- Volatile Keyword
- Interlocked Class
- Memory Barriers
- Thread Synchronization
- Mutex
- Semaphore
- Monitor
- Wait and Pulse
- Thread Interruption
- Thread Priorities
- Thread Affinity
- Thread Lifetime Management
- Thread Pool Configuration
- Thread Pool Optimization
- Task Cancellation Strategy
- Task Fault Handling
- Task Continuation
- Task Prioritization
- Task Dependency
- Task Hierarchies
- Task Scheduling Algorithms
- Task Load Balancing
- Task Distribution
- Task Partitioning
- Task Granularity
- Work Stealing
- Work Sharing
- Work Stealing Algorithms
- Work Sharing Algorithms
- Load Balancing Strategies
- Dynamic Load Balancing
- Static Load Balancing
- Task Queueing
- Task Scheduling Policies
- Task Scheduling Heuristics
- Task Scheduling Metrics
- Task Scheduling Optimization
- Task Scheduling Tuning
- Task Scheduling Performance
- Task Scheduling Scalability
- Task Scheduling Fairness
- Task Scheduling Fairness Metrics
- Task Scheduling Fairness Optimization
- Task Scheduling Fairness Tuning
- Task Scheduling Fairness Performance
- Task Scheduling Fairness Scalability
- Task Scheduling Fairness Heuristics
- Task Scheduling Fairness Metrics
- Task Scheduling Fairness Tuning
- Task Scheduling Fairness Performance
- Task Scheduling Fairness Scalability
- Task Scheduling Fairness Heuristics
- Task Scheduling Fairness Metrics
- Task Scheduling Fairness Tuning
- Task Scheduling Fairness Performance
- Task Scheduling Fairness Scalability
Online Code run
Step-by-Step Guide: How to Implement Tasks and Parallel Programming in C#
Introduction to Tasks and Parallel Programming in C#
Parallel programming allows an application to execute multiple operations concurrently, which can significantly improve performance on multi-core systems. In C#, the System.Threading.Tasks
namespace provides a high-level abstraction for parallel programming, making it easier to work with asynchronous tasks.
Key Concepts:
- Task: Represents an asynchronous operation.
- Task Parallel Library (TPL): A library that simplifies the process of writing parallel code.
- Asynchronous Method Calls: Helps in writing non-blocking, efficient code.
- Parallel.ForEach and Parallel.For: Loops optimized for running iterations in parallel.
Setting Up Your Environment
Ensure you have the following:
- Visual Studio 2022 or Later (or any C# IDE).
- .NET 6 or Later (recommended for the latest features and performance improvements).
Create a new Console Application project to start.
Example 1: Creating and Running a Basic Task
In this example, we'll create a simple task that prints a message to the console.
Step 1: Import the Required Namespace
using System;
using System.Threading.Tasks;
Step 2: Create a Task Using Task.Run
class Program
{
static async Task Main(string[] args)
{
// Create and run a task using Task.Run
Task myTask = Task.Run(() => PrintMessage());
// Wait for the task to complete
await myTask;
Console.WriteLine("Main method continuing...");
}
static void PrintMessage()
{
Console.WriteLine("Hello from the task!");
}
}
Explanation:
- Task.Run: Starts a new task on the thread pool and returns a
Task
object representing that task. - async & await: Allows the
Main
method to be asynchronous. Theawait
keyword waits for the task to complete. - Thread Pool: A collection of reusable threads provided by the .NET Framework to execute short-term tasks.
Output:
Hello from the task!
Main method continuing...
Example 2: Working with Task Results
Now, let's create a task that performs a computation and returns a result.
Step 1: Import the Required Namespaces
using System;
using System.Threading.Tasks;
Step 2: Create a Task that Returns a Result Using Task.Run
class Program
{
static async Task Main(string[] args)
{
// Create and run a task that returns a result
Task<int> sumTask = Task.Run(() => ComputeSum(10));
// Wait for the task to complete and get the result
int result = await sumTask;
Console.WriteLine($"The sum of numbers from 1 to 10 is: {result}");
}
static int ComputeSum(int n)
{
int sum = 0;
for (int i = 1; i <= n; i++)
{
sum += i;
}
return sum;
}
}
Explanation:
- Task
: Represents a task that completes when a result of type int
is available. - ComputeSum: This method calculates the sum of numbers from 1 to
n
. - await sumTask: Waits for the
sumTask
to complete and fetches its result.
Output:
The sum of numbers from 1 to 10 is: 55
Example 3: Creating Tasks with Task.Factory
You can also create tasks explicitly using Task.Factory
.
Step 1: Import the Required Namespaces
using System;
using System.Threading.Tasks;
Step 2: Create and Start a Task Using Task.Factory
class Program
{
static async Task Main(string[] args)
{
// Create a task using Task.Factory
Task myTask = Task.Factory.StartNew(() => PrintMessage());
// Wait for the task to complete
await myTask;
Console.WriteLine("Main method continuing...");
}
static void PrintMessage()
{
Console.WriteLine("Hello from the task created by Task.Factory!");
}
}
Explanation:
- Task.Factory.StartNew: Explicitly starts a new task and returns a corresponding
Task
object. - Task.Factory vs Task.Run:
Task.Run
is generally preferred for starting background tasks as it uses the same infrastructure asParallel.For
andParallel.ForEach
.Task.Factory.StartNew
offers more control but is less commonly used unless specific parameters are needed.
Output:
Hello from the task created by Task.Factory!
Main method continuing...
Example 4: Handling Exceptions in Tasks
When working with tasks, it's crucial to handle exceptions properly.
Step 1: Import the Required Namespaces
using System;
using System.Threading.Tasks;
Step 2: Create Tasks That Might Throw Exceptions
class Program
{
static async Task Main(string[] args)
{
// Create a task that might throw an exception
Task<int> riskyTask = Task.Run(() => DivideNumbers(10, 0));
try
{
// Wait for the task to complete
int result = await riskyTask;
Console.WriteLine($"Result of division: {result}");
}
catch (Exception ex)
{
Console.WriteLine($"Exception caught: {ex.Message}");
}
Console.WriteLine("Main method continuing...");
}
static int DivideNumbers(int dividend, int divisor)
{
return dividend / divisor;
}
}
Explanation:
- DivideNumbers: This method tries to divide
dividend
bydivisor
. Ifdivisor
is zero, it throws aDivideByZeroException
. - try-catch block: Catches and handles exceptions thrown by the task.
Output:
Exception caught: Attempted to divide by zero.
Main method continuing...
Example 5: Running Multiple Tasks Concurrently
Sometimes, you may need to run multiple tasks concurrently and wait for all of them to complete.
Step 1: Import the Required Namespaces
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
Step 2: Create Multiple Tasks
class Program
{
static async Task Main(string[] args)
{
// List to hold tasks
List<Task> tasks = new List<Task>
{
Task.Run(() => PerformOperation("Task 1", 3)),
Task.Run(() => PerformOperation("Task 2", 6)),
Task.Run(() => PerformOperation("Task 3", 2))
};
// Wait for all tasks to complete
await Task.WhenAll(tasks);
Console.WriteLine("All tasks have completed.");
}
static void PerformOperation(string name, int delayInSeconds)
{
Console.WriteLine($"{name} started.");
Task.Delay(delayInSeconds * 1000).Wait(); // Delay in seconds
Console.WriteLine($"{name} finished after {delayInSeconds} seconds.");
}
}
Explanation:
- List
: Stores multiple tasks. - PerformOperation: This method simulates an operation by taking a task name and delay duration.
- Task.WhenAll: Accepts an array or list of tasks and completes when all specified tasks have completed.
Output:
Task 2 started.
Task 3 started.
Task 1 started.
Task 3 finished after 2 seconds.
Task 1 finished after 3 seconds.
Task 2 finished after 6 seconds.
All tasks have completed.
Note: The order of completion may vary since tasks run concurrently.
Example 6: Using Parallel.ForEach for Data Parallelism
If you need to perform operations on a collection of data in parallel, Parallel.ForEach
is your friend.
Step 1: Import the Required Namespaces
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
Step 2: Utilize Parallel.ForEach
class Program
{
static void Main(string[] args)
{
// List of numbers
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
// Use Parallel.ForEach to iterate over the list
Parallel.ForEach(numbers, number =>
{
Console.WriteLine($"Processing number: {number}");
PerformExpensiveOperation(number);
});
Console.WriteLine("All numbers have been processed.");
}
static void PerformExpensiveOperation(int number)
{
// Simulate an expensive operation
Task.Delay(number * 1000).Wait();
Console.WriteLine($"Expensive operation for {number} completed.");
}
}
Explanation:
- Parallel.ForEach: Iterates over each element in the
numbers
list in parallel. - PerformExpensiveOperation: Simulates an operation that takes
number * 1000
milliseconds.
Output:
Processing number: 1
Processing number: 2
Processing number: 3
Processing number: 4
Processing number: 5
Expensive operation for 1 completed.
Expensive operation for 3 completed.
Expensive operation for 2 completed.
Expensive operation for 5 completed.
Expensive operation for 4 completed.
All numbers have been processed.
Note: The order of processing might vary.
Example 7: Using CancellationToken to Cancel Tasks
You can use CancellationToken
to manage the cancellation of tasks.
Step 1: Import the Required Namespaces
using System;
using System.Threading;
using System.Threading.Tasks;
Step 2: Create and Run Tasks with Cancellation Support
class Program
{
static void Main(string[] args)
{
// Create a CancellationTokenSource
CancellationTokenSource cts = new CancellationTokenSource();
// Create a task that supports cancellation
Task myTask = Task.Run(() => PerformLongRunningTask(cts.Token), cts.Token);
Console.WriteLine("Task started. Press any key to cancel it...");
Console.ReadKey();
// Cancel the task
cts.Cancel();
try
{
// Wait for the task to complete
myTask.Wait();
Console.WriteLine("Task completed successfully.");
}
catch (AggregateException ae)
{
foreach (var ex in ae.Flatten().InnerExceptions)
{
if (ex is TaskCanceledException)
{
Console.WriteLine("Task was cancelled.");
}
else
{
Console.WriteLine($"Exception: {ex.Message}");
}
}
}
Console.WriteLine("Main method continuing...");
}
static void PerformLongRunningTask(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
// Check if the token has a cancellation request
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"Task working... Iteration {i + 1}");
Task.Delay(1000).Wait(); // Artificial delay
}
Console.WriteLine("Task completed without being cancelled.");
}
}
Explanation:
- CancellationTokenSource: Used to create a
CancellationToken
that can signal cancellation. - PerformLongRunningTask: This method simulates a long-running task. It periodically checks if a cancellation has been requested and throws a
TaskCanceledException
if so. - Pressing Any Key: Simulates waiting for user interaction before attempting to cancel the task.
- AggregateException: Handles exceptions thrown by tasks. In this case, it catches
TaskCanceledException
if the task was cancelled.
Output:
Scenario 1 (Cancelled):
Task started. Press any key to cancel it...
<Press Any Key>
Task working... Iteration 1
Task working... Iteration 2
Task was cancelled.
Main method continuing...
Scenario 2 (Not Cancelled):
Task started. Press any key to cancel it...
<Don't Press Any Key, Wait for Completion>
Task working... Iteration 1
Task working... Iteration 2
Task working... Iteration 3
Task working... Iteration 4
Task working... Iteration 5
Task working... Iteration 6
Task working... Iteration 7
Task working... Iteration 8
Task working... Iteration 9
Task working... Iteration 10
Task completed without being cancelled.
Main method continuing...
Example 8: Using TaskContinuationOptions to Chain Tasks
Sometimes, you need to chain tasks such that one task runs only after another completes successfully.
Step 1: Import the Required Namespaces
using System;
using System.Threading.Tasks;
Step 2: Chain Tasks Using ContinueWith
class Program
{
static async Task Main(string[] args)
{
// Create and run an initial task
Task<string> initialTask = Task.Run(() => RetrieveData());
// Chain a continuation task that runs only if the initial task completes successfully
Task chainedTask = initialTask.ContinueWith(task =>
{
string data = task.Result;
ProcessData(data);
}, TaskContinuationOptions.OnlyOnRanToCompletion);
// Wait for the chained task to complete
await chainedTask;
Console.WriteLine("Chained task has completed.");
}
static string RetrieveData()
{
Console.WriteLine("Retrieving data...");
Task.Delay(2000).Wait();
Console.WriteLine("Data retrieved successfully.");
return "Sample Data";
}
static void ProcessData(string data)
{
Console.WriteLine($"Processing data: {data}");
Task.Delay(2000).Wait();
Console.WriteLine("Data processed successfully.");
}
}
Explanation:
- RetrieveData: Simulates retrieving data and returns a string.
- ContinueWith: Chains another task (
ProcessData
) to run after the initial task completes. TheTaskContinuationOptions.OnlyOnRanToCompletion
option ensures that the continuation task runs only if the previous task succeeds. - await chainedTask: Ensures that the
Main
method waits for the entire chain to complete before proceeding.
Output:
Retrieving data...
Data retrieved successfully.
Processing data: Sample Data
Data processed successfully.
Chained task has completed.
Example 9: Using Task Parallel Library (TPL) with PLINQ
Parallel LINQ (PLINQ) allows you to easily convert sequential queries into parallel queries.
Step 1: Import the Required Namespaces
using System;
using System.Collections.Generic;
using System.Linq;
Step 2: Convert a LINQ Query to PLINQ
class Program
{
static void Main(string[] args)
{
// List of numbers
List<int> numbers = Enumerable.Range(1, 10).ToList();
// Convert to PLINQ query to process in parallel
var results = numbers.AsParallel()
.Select(number => SquareNumber(number))
.OrderBy(result => result)
.ToList();
// Display results
Console.WriteLine("Squared numbers in ascending order:");
foreach (var result in results)
{
Console.WriteLine(result);
}
}
static int SquareNumber(int number)
{
Console.WriteLine($"Squaring number: {number}");
return number * number;
}
}
Explanation:
- AsParallel(): Converts a collection into a format that supports parallel operations.
- Select and OrderBy: These LINQ methods are executed in parallel when part of a PLINQ query.
- SquareNumber: This method squares a given number and prints the operation for demonstration.
Output:
Squaring number: 1
Squaring number: 2
Squaring number: 3
Squaring number: 4
Squaring number: 5
Squaring number: 6
Squaring number: 7
Squaring number: 8
Squaring number: 9
Squaring number: 10
Squared numbers in ascending order:
1
4
9
16
25
36
49
64
81
100
Note: The squaring operations may occur in any order due to parallel execution, but the final sorted list will always be in ascending order.
Example 10: Using Async/Await for Asynchronous Operations
Asynchronous programming is crucial for non-blocking operations, especially I/O-bound tasks like reading/writing files or making web requests.
Step 1: Import the Required Namespaces
using System;
using System.Net.Http;
using System.Threading.Tasks;
Step 2: Perform Asynchronous HTTP Request
class Program
{
// Declare an HttpClient instance
private static readonly HttpClient httpClient = new HttpClient();
static async Task Main(string[] args)
{
Console.WriteLine("Starting asynchronous HTTP request...");
// Call an asynchronous method
string response = await FetchDataFromWebAsync("https://jsonplaceholder.typicode.com/posts/1");
Console.WriteLine("HTTP request completed successfully.");
Console.WriteLine($"Response:\n{response}");
}
static async Task<string> FetchDataFromWebAsync(string url)
{
// Perform the GET request asynchronously
HttpResponseMessage responseMessage = await httpClient.GetAsync(url);
// Ensure the request succeeded
responseMessage.EnsureSuccessStatusCode();
// Read the response body asynchronously
string responseBody = await responseMessage.Content.ReadAsStringAsync();
return responseBody;
}
}
Explanation:
- HttpClient: A class used to send HTTP requests and receive HTTP responses.
- FetchDataFromWebAsync: This method performs an asynchronous HTTP GET request to the specified URL and reads the response content asynchronously.
- await: Non-blocking call that allows other code to execute while waiting for the asynchronous operation to complete.
Output:
Starting asynchronous HTTP request...
HTTP request completed successfully.
Response:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
Best Practices:
- Avoid Blocking Calls: Never call
.Wait()
or.Result
within an async method, as this can cause deadlocks. - Use HttpClient Correctly: Instantiate
HttpClient
once and reuse it throughout the application to avoid socket exhaustion issues.
Advanced Example: Downloading Images Concurrently
In this complex example, we'll download multiple images concurrently and save them to disk.
Step 1: Import the Required Namespaces
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
Step 2: Define URLs and Destination Folder
class Program
{
// URLs of images to download
private static readonly List<string> imageUrls = new List<string>
{
"https://via.placeholder.com/150",
"https://via.placeholder.com/200",
"https://via.placeholder.com/250",
"https://via.placeholder.com/300"
};
// Destination folder to save images
private static readonly string destinationFolder = @"C:\DownloadedImages";
// HttpClient instance
private static readonly HttpClient httpClient = new HttpClient();
static async Task Main(string[] args)
{
try
{
// Ensure the destination folder exists
Directory.CreateDirectory(destinationFolder);
// List of tasks for downloading images
List<Task> downloadTasks = new List<Task>();
// Create a task for each image URL
foreach (var url in imageUrls)
{
Task downloadTask = DownloadImageAsync(url, destinationFolder);
downloadTasks.Add(downloadTask);
}
// Wait for all download tasks to complete
await Task.WhenAll(downloadTasks);
Console.WriteLine("All images have been downloaded successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
static async Task DownloadImageAsync(string imageUrl, string folderPath)
{
// Get the image file name from the URL
string fileName = Path.GetFileName(new Uri(imageUrl).AbsolutePath);
string filePath = Path.Combine(folderPath, fileName);
Console.WriteLine($"Downloading {fileName} from {imageUrl}...");
// Send the HTTP request and get the response stream
HttpResponseMessage response = await httpClient.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
using (Stream contentStream = await response.Content.ReadAsStreamAsync(),
fileStream = new FileStream(filePath, FileMode.Create))
{
await contentStream.CopyToAsync(fileStream);
}
Console.WriteLine($"{fileName} downloaded and saved to {filePath}.");
}
}
Explanation:
- imageUrls: A list containing URLs of images to download.
- destinationFolder: The directory where downloaded images will be saved.
- DownloadImageAsync: An asynchronous method that downloads an image from a URL and saves it to the specified folder.
- Path.GetFileName: Extracts the file name from the given URL.
- HttpClient.GetAsync: Sends an asynchronous GET request.
- ReadAsStreamAsync: Reads the response content as a stream.
- FileStream: Opens a file stream to write the image data to disk.
- CopyToAsync: Copies data from the response stream to the file stream asynchronously.
- Task.WhenAll: Ensures all download tasks complete before the program continues.
Output:
Downloading 150 from https://via.placeholder.com/150...
Downloading 200 from https://via.placeholder.com/200...
Downloading 250 from https://via.placeholder.com/250...
Downloading 300 from https://via.placeholder.com/300...
150 downloaded and saved to C:\DownloadedImages\150.
200 downloaded and saved to C:\DownloadedImages\200.
250 downloaded and saved to C:\DownloadedImages\250.
300 downloaded and saved to C:\DownloadedImages\300.
All images have been downloaded successfully.
Additional Notes:
- Error Handling: Consider adding more robust error handling, such as retry logic or specific exception handling for different types of errors.
- Resource Management: Always release resources properly. Here,
FileStream
is disposed automatically using theusing
statement.
Summary
In this guide, we covered various aspects of tasks and parallel programming in C# using comprehensive examples:
- Creating and Running Basice Tasks
- Working with Task Results
- Using Task.Factory
- Handling Exceptions in Tasks
- Running Multiple Tasks Concurrently
- Using Parallel.ForEach for Data Parallelism
- Using CancellationToken to Cancel Tasks
- Chaining Tasks with TaskContinuationOptions
- Using TPL with PLINQ
- Using Async/Await for Asynchronous Operations
- Downloading Images Concurrently (Advanced Example)
By understanding these concepts and practices, you can write efficient, scalable, and responsive .NET applications that take full advantage of multi-core processors.
Feel free to experiment with these examples and modify them to suit your needs. Happy coding!
References:
Top 10 Interview Questions & Answers on Tasks and Parallel Programming in C#
Top 10 Questions and Answers on Tasks and Parallel Programming in C#
1. What are the benefits of using Tasks in C#?
- Easier to Manage: Tasks are higher-level abstractions compared to threads, making concurrent programming simpler.
- Improved Resource Utilization: The Task Parallel Library (TPL) manages threads efficiently, reusing existing threads and reducing overhead.
- Structured Task Parallelism: Tasks support structured parallelism, enabling easier reasoning about code correctness and debugging.
2. How do I create a Task in C#?
You can create a task using the Task.Run()
or Task.Factory.StartNew()
methods. Here’s an example with Task.Run()
:
Task myTask = Task.Run(() => {
// Code to execute in parallel
Console.WriteLine("Running in parallel!");
});
3. What is the difference between Task.Run()
and Task.Factory.StartNew()
?
Task.Run()
: Starts a new task and schedules it on the thread pool. It's more straightforward and generally preferable for simple scenarios.Task.Factory.StartNew()
: Offers more control, such as setting creation and scheduling options, but it can lead to more complex code. Avoid using it unless you need specific features.
4. How can I handle exceptions in a Task?
Exceptions in tasks are propagated and can be handled using try-catch blocks:
Task myTask = Task.Run(() => {
throw new InvalidOperationException("Unexpected error");
});
try {
myTask.Wait(); // Wait for the task to complete
} catch (AggregateException ae) {
ae.Handle(ex => {
Console.WriteLine("Exception: " + ex.Message);
return true; // Indicate that the exception is handled
});
}
5. What is the difference between Task.Wait()
and await
?
Task.Wait()
: Synchronously waits for the task to complete, which can block the calling thread.await
: Asynchronously waits for the task, allowing the calling thread to continue and avoid blocking.await
is generally preferred for non-blocking asynchronous programming.
6. How do you ensure task continuation in C#?
You can use Task.ContinueWith()
to specify actions to be performed once a task finishes:
Task myTask = Task.Run(() => {
Console.WriteLine("Initial Task");
});
Task continuationTask = myTask.ContinueWith(task => {
Console.WriteLine("Continuation Task");
});
7. What is Parallel.For and when would you use it?
Parallel.For
provides data parallelism for loops. It can be used when you need to perform the same operation across a range of data concurrently:
Parallel.For(0, 10, i => {
Console.WriteLine("Processing " + i);
});
8. How does the CancellationToken
work in C#?
A CancellationToken
is used to request cooperative cancellation of operations:
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = cancellationTokenSource.Token;
Task myTask = Task.Run(() => {
for (int i = 0; i < 100; i++) {
cancellationToken.ThrowIfCancellationRequested(); // Throws if cancellation requested
Console.WriteLine(i);
}
}, cancellationToken);
cancellationTokenSource.Cancel(); // Request cancellation
9. What is the difference between Parallel.ForEach
and Parallel.For
?
Parallel.For
: Suitable for scenarios where you need to iterate over a range of numbers.Parallel.ForEach
: Ideal for iterating over collections, providing a more flexible and often more readable approach:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
Parallel.ForEach(numbers, number => {
Console.WriteLine(number);
});
10. How can I ensure that tasks run in a specific order?
To run tasks in a specific order, you can use task continuations or simply use await
:
Login to post a comment.