Understanding Concurrency in C#

During the interview process, I was questioned about concurrency, asynchronous programming, parallel programming, CPU-bound and I/O-bound operations, and multithreading multiple times.

These, in my opinion, are the most significant subjects and, when it comes down to it, the most difficult to understand and articulate.

I’ll cover everything you need to know about concurrency and its subtypes in this instructional series. Let’s get started.

Describe Concurrency

“Doing more than one work at a given time” is known as concurrency. It should be possible for us to execute another procedure while the first is in progress. Generally speaking, an application is “responsible” enough to let us work on another activity while it is executing one operation.

Modern applications, particularly those involving I/O-bound activities or user interactions, require concurrency to improve responsiveness and speed.

The behind-the-scenes logic for the above diagram is: “While finishing the first request, respond to the second request.” That is what concurrency looks like.

Let’s take one simple example.

While uploading to the server, we can get a response from the database.

CPU-bound and I/O-bound operations

Before diving into details, we need to understand the terminology to explore Concurrency better.

One of the terminologies is CPU-bound and I/O-bound operations.

CPU-Bound Operations

Characteristics

  1. Consume significant CPU resources for calculations and processing.
  2. They spend most of their time actively using the CPU.
  3. Typically involves complex algorithms, data processing, mathematical computations, or cryptography.

Examples

  • Image processing
  • Data compression
  • Matrix computations
  • Scientific simulations
  • Password hashing

Handling Techniques

  • Parallelism: Distribute work across multiple CPU cores using the Task Parallel Library (TPL) and techniques like Parallel.For and Parallel.ForEach.
  • Thread pools: Use thread pools to manage multiple threads efficiently, avoiding the overhead of creating and destroying threads for each operation.
class Program
{
    static void Main(string[] args)
    {
        Stopwatch stopwatch = Stopwatch.StartNew();

        // Simulate a CPU-intensive calculation using Parallel.ForEach
        Parallel.ForEach(Enumerable.Range(1, 100000), index =>
        {
            // Perform a computationally expensive task
            Math.Pow(index, index) * Math.Sin(index);
        });

        stopwatch.Stop();
        Console.WriteLine($"Total time elapsed: {stopwatch.ElapsedMilliseconds} milliseconds");
    }
}

I/O-Bound Operations

Characteristics

  1. Spend most of their time waiting for input/output operations to complete.
  2. Involve interactions with external resources like databases, file systems, networks, or user input.
  3. CPU usage is relatively low during these wait periods.

Examples

  • Reading/writing files
  • Making network calls
  • Accessing databases
  • Handling user input

Handling Techniques

  • Asynchronous programming: Use async and await keywords to enable non-blocking operations, allowing other code to execute while waiting for I/O.
  • Asynchronous I/O APIs: Leverage I/O-bound APIs that support asynchronous operations, such as HttpClient for network requests or FileStream for file I/O.
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace IOBoundExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            try
            {
                Stopwatch stopwatch = Stopwatch.StartNew();

                // Asynchronously download multiple files using HttpClient
                await Task.WhenAll(new[]
                {
                    DownloadFileAsync("https://example.com/file1.txt"),
                    DownloadFileAsync("https://example.com/file2.jpg"),
                    DownloadFileAsync("https://example.com/file3.pdf")
                });

                stopwatch.Stop();
                Console.WriteLine($"Total time elapsed: {stopwatch.ElapsedMilliseconds} milliseconds");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error occurred: {0}", ex.Message);
            }
        }

        static async Task DownloadFileAsync(string url)
        {
            using (HttpClient client = new HttpClient())
            {
                using (HttpResponseMessage response = await client.GetAsync(url))
                {
                    if (response.IsSuccessStatusCode)
                    {
                        using (Stream stream = await response.Content.ReadAsStreamAsync())
                        {
                            // Simulate file processing or saving
                            await ProcessFileAsync(stream);
                        }
                    }
                    else
                    {
                        Console.WriteLine($"Failed to download {url}: {response.StatusCode}");
                    }
                }
            }
        }

        static async Task ProcessFileAsync(Stream stream)
        {
            // Simulate file processing logic here
            await Task.Delay(500); // Simulate processing time
        }
    }
}

Want to get best practices?

  1. Choose appropriate techniques based on operation type: Use parallelism for CPU-bound tasks and async/await for I/O-bound tasks.
  2. Avoid blocking the main thread: Use async/await or background threads to prevent UI responsiveness issues.
  3. Measure and profile: Use profiling tools to identify performance bottlenecks and determine the best optimization strategies.
  4. Understand the underlying hardware: Consider the number of CPU cores and I/O capabilities of the system when choosing techniques.
  5. Design for concurrency: Structure code with concurrency in mind to avoid race conditions and deadlocks.
  6. Use appropriate synchronization mechanisms: Protect shared resources with locks, mutexes, or other synchronization constructs.

Ok, then what is the relation between CPU-bound, I/O-bound operations with Concurrency?

All these types of operations (CPU bound and I/O bound) together create the world of concurrency. It doesn’t matter if it is CPU intensive or I/O relying operation. They are concurrency by their nature.

This was just an introduction. Starting from the next article, we’re going to the implementation forms of Concurrency in C#.

Best and Most Recommended ASP.NET Core 8 Hosting

Fortunately, there are a number of dependable and recommended web hosts available that can help you gain control of your website’s performance and improve your ASP.NET Core 8 web ranking. HostForLIFEASP.NET is highly recommended. In Europe, HostForLIFEASP.NET is the most popular option for first-time web hosts searching for an affordable plan.

Their standard price begins at only € 3.49 per month. Customers are permitted to choose quarterly and annual plans based on their preferences. HostForLIFEASP.NET guarantees “No Hidden Fees” and an industry-leading ’30 Days Cash Back’ policy. Customers who terminate their service within the first thirty days are eligible for a full refund.

By providing reseller hosting accounts, HostForLIFEASP.NET also gives its consumers the chance to generate income. You can purchase their reseller hosting account, host an unlimited number of websites on it, and even sell some of your hosting space to others. This is one of the most effective methods for making money online. They will take care of all your customers’ hosting needs, so you do not need to fret about hosting-related matters.