Skip to content

Concepts explanations and code examples of how threads, CPU cores, asynchronous programming and related topics work in .Net Core.

License

Notifications You must be signed in to change notification settings

phdguigui/ParallelUniverses

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌟 In memoriam Pamela Cristina Bins 🌸

"Life has always been rosy with you."

ParallelUniverses πŸŒŒπŸš€

image0_0

Welcome to the Parallel Computing Repository πŸš€πŸŒŒ

This repository is dedicated to exploring the concepts of parallelism, concurrency, multithreading, and thread management in computing. Whether you're working with a single-core CPU or leveraging the power of a multicore processor, understanding these concepts is essential for building efficient and high-performance applications.

In this repository, you'll find detailed explanations, examples, and practical use cases related to:

  • CPU Architecture: Learn about multicore and single-core systems, and how processes are managed in each scenario.
  • Threads and Multithreading: Understand the role of threads in multitasking, and how multithreading enhances performance by executing multiple tasks concurrently.
  • Thread Pools: Discover how thread pools help manage resources efficiently by reusing threads for short-lived tasks.
  • .NET Examples: Practical examples in C# showcasing how to implement parallelism, concurrency, and multithreading.

By diving into this repository, you'll gain insights into key performance optimization techniques and how to effectively manage resources, especially in systems with high concurrency demands like web servers and cloud applications. 🌐⚑

Feel free to explore the content, contribute, and ask any questions you may have! Together, let's unlock the power of parallel computing! πŸ’»πŸ”§

CPU πŸ–₯️

The CPU is responsible for executing, processing, and computing tasks, programs, and processes using threads. As mentioned by Rainer Stropek (I strongly recommend you to watch this professor's classes), it is now common to use a single-core server for web hosting. For example, with a CPU that has 4 cores, you can divide these 4 cores into 4 different virtual machines, each with 1 core, allowing you to run more virtual machines. But how does the CPU execute processes? There are several methods for doing so, which are explained below.

Multicore πŸ§‘β€πŸ”¬

When you have a machine with multiple CPU cores, you can execute multiple processes literally at the same time. Think of each CPU core as a worker and the CPU as a factory. With multiple workers, you can have multiple processes running simultaneously. Each worker operates independently, and the processes do not depend on each other.

Parallelism πŸ”„

Parallelism is based on the concept of multicore systems, where you can execute multiple processes on different CPU cores. For example, if you have 4 processes running on your machine, you can assign each one to a different core, allowing them to run independently of one another.

Single-core πŸ•ΉοΈ

It’s easy to understand how a machine executes multiple processes with a multicore CPU, but how is this possible with only one CPU core? This is where the concept of concurrency comes into play.

Concurrent Execution ⏳

When there's only one core to execute multiple processes, it might seem impossible to do so. However, this method of execution is more common than you think. With a single core, the CPU rapidly switches between processes, giving the impression that they are running in parallel. However, each process is executed in its own time slice, making use of the CPU at different moments. You could say that processes "compete" for CPU time.

Threads 🧡

A thread is the smallest unit of execution within a process. While a process is an instance of a program being executed, a thread represents a single path of execution within that process. Threads are often referred to as "lightweight processes" because they share the same memory space and resources as their parent process, unlike independent processes, which have their own memory space. In modern computing, threads allow for more efficient use of resources, enabling the concurrent execution of tasks within a process.

Multithreading πŸ”€

Multithreading is the ability of a CPU (or a single core) to manage multiple threads simultaneously. This is achieved by breaking down a larger task into smaller sub-tasks that can run independently of one another, leading to more efficient processing. In a multithreaded application, different threads can execute different parts of the program concurrently, improving the overall performance of the system.

Although each thread runs sequentially within a process, a system can switch between threads so quickly (via context switching) that it appears they are executing simultaneously. This is especially important on systems with a single-core CPU, where true parallelism isn’t possible, but the illusion of concurrency is maintained through rapid switching between threads.

In summary, threads are essential components in modern computing, enabling processes to execute multiple tasks either simultaneously or concurrently. Whether on a single-core or multicore CPU, threads improve the performance and responsiveness of applications, making them a fundamental part of multitasking systems.

Thread Pool πŸ”„πŸ’‘

A thread pool is a collection of pre-instantiated, reusable threads that can be used to perform a set of tasks concurrently. Instead of creating a new thread for every individual taskβ€”which can be resource-intensive and inefficientβ€”a thread pool allows tasks to be assigned to available threads within the pool. Once a thread finishes executing a task, it returns to the pool, ready to handle new tasks.

This approach reduces the overhead of thread creation and destruction, improving performance and resource management. Thread pools are especially useful in applications that need to handle many short-lived tasks, such as server applications and web services. Using a thread pool optimizes throughput and scalability in concurrent programming environments.

When to Create a New Thread vs. Use a Thread Pool πŸ€”

You should create a new thread when a task is unique, long-running, or requires specific resources that cannot be shared efficiently with other tasks. However, for tasks that are short-lived, repetitive, or can be shared across multiple invocations, using a thread from a thread pool is more efficient. πŸƒβ€β™‚οΈπŸ’¨

Comparisons βš–οΈπŸ”

Mechanisms βš™οΈ

Mechanism Creates New Thread? Uses ThreadPool? Executes Asynchronously? Releases Main Thread? Best Use
Thread.Start() βœ… Yes ❌ No ❌ No (blocks until finished) ❌ No Execution of independent tasks requiring full control over the thread.
Task.Run() ❌ No (reuses) βœ… Yes βœ… Yes βœ… Yes Perform computationally intensive tasks without blocking the main thread.
ThreadPool.QueueUserWorkItem() ❌ No (reuses) βœ… Yes βœ… Yes (but without Task) βœ… Yes Execution of short tasks with no need for a return value.
async ❌ No (just a modifier) ❌ No βœ… Yes ⚠️ Depends on internal code Allows a method to support await, useful for I/O operations.
await ❌ No (just waits) ❌ No βœ… Yes βœ… Yes Pauses execution until the Task completes without blocking the thread.
Parallel.ForEach() ❌ No (uses existing threads) βœ… Yes βœ… Yes ⚠️ Depends on code Executes loops in parallel, useful for heavy computations.

I/O Bound vs CPU Bound πŸ’»πŸ”„

Factor I/O-bound CPU-bound
Nature of Task Involves waiting for external resources (e.g., disk I/O, network requests). Involves intensive computations that use CPU power.
Thread Usage Often uses asynchronous methods like async/await or Task.Run() to avoid blocking the thread. Typically requires dedicated threads (e.g., Thread, ThreadPool, Parallel.ForEach) for maximum CPU utilization.
Blocking vs. Non-blocking Non-blocking execution is ideal. Use asynchronous methods to free up the main thread. Blocking execution is common, especially for long-running computations. Parallel execution can be used to improve performance.
Execution Style Asynchronous tasks (e.g., async/await, Task) allow the system to perform other work while waiting for I/O operations. Often synchronous or parallelized with Parallel.ForEach or Task.Run() for parallel processing to maximize CPU utilization.
Impact on Main Thread Main thread remains free to continue other tasks (e.g., UI updates). Main thread may be blocked if computation is not handled properly (but can be offloaded to background threads).
Best Use Case Web requests, file I/O, database queries, network operations, or any task that spends time waiting for external resources. Data processing, heavy mathematical computations, image/video rendering, simulations, or tasks that require extensive use of CPU.
Example Technologies async/await, HttpClient, file system operations, Task.Run(), ThreadPool.QueueUserWorkItem(). Parallel.ForEach(), Thread, Task.Run(), multi-threaded algorithms.

.NET Examples πŸ’»πŸ”§

1. Multicore and Parallelism (Using Multiple Cores) πŸ–₯οΈβš™οΈ

In C#, you can leverage multiple cores through parallel programming, especially using the Parallel class. This allows you to execute multiple tasks simultaneously across available CPU cores.

int[] numbers = { 1, 2, 3, 4, 5 };

Parallel.ForEach(numbers, (number) =>
{
    Console.WriteLine($"Processing number {number} on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}");
});

For example, Parallel.ForEach can be used to process a collection of data concurrently, where each task can be executed on a different core, improving performance by dividing the work among available cores.

2. Single-core and Concurrent Execution πŸ”„

In single-core machines, multiple tasks can be executed concurrently, but not truly in parallel. The CPU quickly switches between tasks, giving the illusion of parallelism. This is achieved through multithreading.

static async Task Main()
{
    // Simulating concurrent execution with async/await on a single core
    Task task1 = Task.Run(() => DoWork("Task 1"));
    Task task2 = Task.Run(() => DoWork("Task 2"));

    await Task.WhenAll(task1, task2);
}

static async Task DoWork(string taskName)
{
    Console.WriteLine($"{taskName} started on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(1000);  // Simulate async work
    Console.WriteLine($"{taskName} finished on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}");
}

The async and await keywords allow for non-blocking concurrency. Even on a single-core CPU, tasks appear to run concurrently, thanks to rapid context switching between them.

3. Multithreading and Thread Creation πŸ§΅πŸ’»

Creating threads manually is useful when you need full control over the thread, especially for long-running or resource-specific tasks. You can manually create a new thread using the Thread class and start it to perform a specific task.

static void Main()
{
    // Creating a new thread to run a task
    Thread newThread = new Thread(() => DoWork("New Thread"));
    newThread.Start();

    Console.ReadLine();
}

static void DoWork(string threadName)
{
    Console.WriteLine($"{threadName} started on thread {Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(1000);  // Simulate work
    Console.WriteLine($"{threadName} finished on thread {Thread.CurrentThread.ManagedThreadId}");
}

This approach is beneficial when tasks are resource-intensive or need to run independently of others. However, manually managing threads can be inefficient compared to using a thread pool for short-lived tasks.

4. Thread Pool πŸŠβ€β™‚οΈπŸ”„

Thread pools are great for executing many short-lived tasks concurrently. The thread pool reuses threads instead of creating new ones, improving efficiency and resource usage. Instead of creating new threads for every task, tasks are assigned to available threads from the pool.

static void Main()
{
    // Queuing a task to the thread pool
    ThreadPool.QueueUserWorkItem((state) => DoWork("Thread Pool Task"));

    Console.ReadLine();
}

static void DoWork(string taskName)
{
    Console.WriteLine($"{taskName} started on thread {Thread.CurrentThread.ManagedThreadId}");
    Thread.Sleep(1000);  // Simulate work
    Console.WriteLine($"{taskName} finished on thread {Thread.CurrentThread.ManagedThreadId}");
}

Thread pools are ideal for scenarios like server applications and web services where many tasks need to be executed quickly and concurrently, reducing the overhead of thread creation.

5. Task Parallel Library (Task.Run) πŸƒβ€β™‚οΈπŸ’¨

Task.Run provides an easy way to run tasks concurrently using the thread pool. It's ideal for performing background work without blocking the main thread, especially in scenarios where you need non-blocking behavior such as UI applications.

static void Main()
{
    // Running tasks using the thread pool through Task.Run
    Task.Run(() => DoWork("Background Task"));

    Console.WriteLine("Main thread continues to execute while the background task runs...");
    Console.ReadLine();
}

static void DoWork(string taskName)
{
    Console.WriteLine($"{taskName} started on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}");
    Task.Delay(1000).Wait();  // Simulate async work
    Console.WriteLine($"{taskName} finished on thread {System.Threading.Thread.CurrentThread.ManagedThreadId}");
}

By using Task.Run, you can offload tasks to the thread pool, which helps improve responsiveness and performance. Tasks executed this way are automatically managed by the framework, removing the need to manually create and manage threads.

6. Using async and await for Asynchronous Execution β³πŸ”„

The async modifier allows methods to execute asynchronously. This is particularly useful for I/O-bound tasks, such as reading from a file or making network requests, without blocking the main thread. async methods work in combination with await to keep the program responsive while tasks are completed in the background.

static async Task Main()
{
    // Calling an asynchronous method
    await PerformAsyncTask();

    Console.WriteLine("Main thread continues after async task.");
}

static async Task PerformAsyncTask()
{
    Console.WriteLine("Async task started.");
    await Task.Delay(2000);  // Simulate async work
    Console.WriteLine("Async task finished.");
}

With async and await, tasks can be paused until a result is ready, but they don't block other operations from continuing.

Conclusion 🌟

In summary, process and thread management is essential for the efficiency and performance of modern systems. The use of multiple cores (parallelism) allows for the simultaneous execution of tasks, while concurrent execution on single-core machines is possible through techniques like rapid context switching between processes and threads. Multithreading allows a process to be broken down into smaller subtasks, improving resource utilization and system performance, especially in scenarios with many short tasks.

Choosing between creating new threads or using a thread pool depends on the nature of the tasks. For short and repetitive tasks, thread pools are more efficient, while for long-running tasks or those requiring specific resources, manually creating threads might be necessary. Additionally, the use of async and await enables asynchronous execution, optimizing I/O-bound operations without blocking the main process, keeping the application responsive. ⚑

Understanding these techniques and when to apply them can have a significant impact on the performance and scalability of systems, especially in high-demand environments like servers and web services. πŸŒπŸš€


Developed by Guilherme Siedschlag πŸ‘“

About

Concepts explanations and code examples of how threads, CPU cores, asynchronous programming and related topics work in .Net Core.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages