Exploring Parallelism in C#: Tasks, Asynchrony, and Concurrency
Introduction to Parallelism
Parallelism in programming is the ability to execute multiple tasks simultaneously, which is essential for maximizing system resource utilization and improving computational efficiency. By dividing a task into smaller parts and executing them in parallel, we can speed up processing and more effectively handle intensive operations.
Code Example:
class Program
{
static void Main(string[] args)
{
string[] zipCodes = ["07155081", "15800100", "38407369", "77445100",
"78015818", "77020514", "62050500", "57042080",
"57071840", "75065150", "88352020", "66030510"];
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = 4
};
var stopWatch = new Stopwatch();
stopWatch.Start();
Parallel.ForEach(zipCodes, parallelOptions, zipCode =>
{
Console.WriteLine(new ViaCepService().GetCep(zipCode) +
$" - Thread: {Environment.CurrentManagedThreadId}");
});
stopWatch.Stop();
Console.WriteLine($"The total processing time is " +
$"{stopWatch.ElapsedMilliseconds} ms");
}
}
In this example, efficiency is significantly improved when fetching data from a ZIP code API. The Parallel.ForEach
method is used to make multiple requests to the API simultaneously, which greatly speeds up the data retrieval process. To ensure proper control of this parallel process, a limit of four threads is set using ParallelOptions
. Each of these threads is responsible for making a request to the API for a specific ZIP code. After all operations are completed, the total processing time is displayed in the console. This approach allows the program to run faster by maximizing system resources and executing multiple tasks concurrently.
Tasks and async/await
Developing software, especially for web applications, can be challenging due to the complexity of the operations involved. For example, when querying a third-party API or processing large volumes of data, the time required to complete these operations can affect the user experience. This can result in a poorly performing application or even an unresponsive one.
To mitigate this problem, we resort to using asynchronous techniques such as Task
, async
, and await
. These mechanisms allow the application to continue executing other tasks while awaiting the completion of asynchronous operations. Thus, even in the face of operations that require considerable time, the application maintains its responsiveness, providing a smoother and more satisfactory experience for the end user.
Alright, but what is a Task and async/await?
Tasks are units of asynchronous work in .NET that represent operations that can be executed in the background. When started, these tasks are added to a queue of threads known as the thread pool. Each task is executed on a thread from the pool, which is an execution unit designated for a specific operation.
On the other hand, async and await are keywords used in asynchronous methods in C#. The async modifier indicates that a method contains asynchronous operations, while await is used to pause the execution of the method until an asynchronous operation is completed. This allows the thread to be freed to perform other tasks while waiting for the result of the asynchronous operation. Thus, the application continues to respond to other requests and executions, allowing other operations to occur concurrently.
Code Example:
class Program
{
static async Task Main(string[] args)
{
var taskA = MethodAAsync();
var taskB = MethodBAsync();
await Task.WhenAll(taskA, taskB);
Console.WriteLine($"TaskA id = {taskA.Id} " +
$"Thread: {Environment.CurrentManagedThreadId}");
Console.WriteLine($"TaskB id = {taskB.Id} " +
$"Thread: {Environment.CurrentManagedThreadId}");
Console.WriteLine("Completed all tasks");
}
static async Task MethodAAsync()
{
Console.WriteLine("Method A");
await Task.Delay(1000);
}
static async Task MethodBAsync()
{
Console.WriteLine("Method B");
await Task.Delay(500);
}
}
In the Main
method, two asynchronous tasks are started: MethodAAsync()
and MethodBAsync()
. Then, Task.WhenAll(taskA, taskB)
is used to await the completion of both tasks.
Each method (MethodAAsync()
and MethodBAsync()
) simulates an asynchronous operation using Task.Delay(), which suspends the task execution for a specified period of time.
After waiting for the completion of both tasks, the IDs of the tasks (taskA.Id
and taskB.Id
) and the thread IDs (Environment.CurrentManagedThreadId
) are printed on the screen. Finally, a message is displayed indicating that all tasks have been completed.
Understanding Concurrency
Concurrency in computer science refers to the ability of multiple processes or threads to execute simultaneously in a shared system. This allows for more efficient utilization of system resources, such as CPU and memory. However, concurrency can lead to issues such as race conditions and concurrent access to shared resources, which need to be managed through synchronization techniques like locks and semaphores.
Do you still not understand?
Imagine a library where several students are trying to check out books at the same time. In this situation, each student represents a process or thread in an operating system, while the books are shared resources. Now, consider the scenario where two students try to check out the same book simultaneously. This can cause problems, as both contend for access to the shared resource, resulting in conflicts and inconsistencies.
To avoid these issues, synchronization techniques are necessary. These techniques ensure that access to resources is organized and controlled properly, preventing conflicts between processes or threads. In this context, the librarian plays the role of a system manager, coordinating the students' requests to ensure that each has access to the books in an orderly and conflict-free manner. This coordination is essential to ensure a smooth experience for all library users.
Conclusion
Mastering parallelism, tasks, async/await, and concurrency is essential for creating efficient and error-free programs. These concepts empower optimized utilization of system resources, enabling simultaneous task execution and avoiding conflicts between processes or threads, thus ensuring robust and reliable performance.