Grokking the Coding Interview

Multithreading and Concurrency Concepts I Wish I Knew Before the Interview

A Comprehensive Guide to Multithreading and Concurrency for Beginners.

Arslan Ahmad
Level Up Coding
Published in
14 min readJun 11, 2023

--

The System Design Interview Roadmap

What is Multithreading?

Imagine you are sitting in your office and you have multiple tasks to do — reading emails, making phone calls, writing reports, meeting colleagues, and so on. Now, if you were to do these tasks one by one, it would take a good amount of time, right? But what if we could do several of these tasks at once? Like reading an email during a dull meeting (we’ve all been there!), or making a phone call while going through the report. This simultaneous execution of tasks not only makes us more productive but also saves precious time. This is, in the simplest terms, the essence of multithreading.

In the realm of computing, a thread is the smallest sequence of programmed instructions that can be managed independently by a scheduler. Multithreading is the ability of a central processing unit (CPU) (or a single core in a multi-core processor) to provide multiple threads of execution concurrently, supported by the operating system.

To put it in perspective, let’s look at a real-life example. Consider a web browser — it’s a multi-threaded application. When we open multiple tabs, each tab is handled by a separate thread. So, while one tab is loading a webpage, we can continue scrolling or reading on another. This is multithreading at work!

What is Concurrency?

Now that we’ve touched upon multithreading, let’s introduce another intriguing concept — Concurrency.

Imagine you’re cooking a meal. You chop the vegetables, heat the oil, sauté the veggies, and then let them cook. While the veggies are cooking, you start kneading the dough for bread. Here, even though the tasks are related and dependent, you’re not waiting for one to finish completely before starting the next. This ability to overlap tasks is called concurrency.

In the world of computing, concurrency is the execution of the multiple instruction sequences at the same time. It happens in the operating system when there are several process threads running in parallel. The running process threads always communicate with each other through shared memory or message passing. Concurrency results in sharing of resources, in turn making the system efficient.

As an example, let’s consider online banking. When we log in to our account, we can view our balance, make transactions, and maybe even chat with customer support. All these tasks can occur simultaneously and independently. That’s concurrency.

Why is Multithreading and Concurrency Important?

In today’s world, where time is of the essence and efficiency is key, concepts like multithreading and concurrency have gained a lot of traction. They not only help in improving the speed and performance of tasks but also improve the overall system efficiency.

Multithreading helps to achieve parallelism in the programs. We can use it to keep the CPU busy and exploit its full potential. For instance, in a game, one thread could handle the game’s graphics, another could handle the user inputs, and yet another could handle the game logic.

Similarly, concurrency helps to interleave the execution of different tasks and maximizes the utilization of the CPU. It allows systems to handle multiple requests and operations, thereby improving throughput and responsiveness. For example, in a web server, each request can be handled independently by a different thread, thus providing concurrent access to multiple users.

By now, we should have a fundamental understanding of what multithreading and concurrency are, and why they matter. In the coming sections, we’ll delve deeper into these concepts, explore their applications, and learn about the best practices involved.

If you like this article, join my newsletter.

Importance of Multithreading and Concurrency

As we journey deeper into the captivating realm of multithreading and concurrency, it becomes essential to understand why they hold such importance in the world of computing. In this section, let’s focus on the whys of multithreading and concurrency and how they contribute to an efficient and effective computing experience.

A. Why Do We Need Multithreading?

Imagine you’re trying to watch your favorite show on a streaming platform, and every time the scene changes, the video buffers. Or consider an application that becomes unresponsive when it’s processing a hefty task. How frustrating would that be? Multithreading is the superhero that prevents these situations.

Let’s understand why multithreading is crucial:

1. Enhanced Performance and Responsiveness

Multithreading allows multiple threads of a process to run concurrently, enhancing the performance of a system, especially on multi-core and multi-processor machines. It’s like having a team working on a project instead of an individual — with each member focusing on a particular task, the project gets completed faster.

Further, by allowing tasks to run concurrently, multithreading ensures that the application remains responsive. Even if a thread is busy, other threads can continue their tasks, thus preventing the application from freezing.

For instance, in a word processor, while one thread performs a spell check, another could handle user inputs, preventing any lag in the user experience.

2. Better Resource Utilization

With multithreading, different threads of a single process share the same data space, leading to efficient utilization of resources. Sharing the resources reduces the overhead of memory duplication and synchronization, resulting in improved system productivity.

3. Economical and Robust

Threads are lighter than processes since they share the same address space, resulting in a more economical use of resources. Also, a problem in one thread doesn’t affect the rest of the threads. Hence, it makes the system more robust.

B. Why Do We Need Concurrency?

Imagine being at an amusement park, and despite several games and rides, everyone is allowed to participate in only one activity at a time. That would lead to massive queues and disgruntled visitors, right? This is where concurrency steps in, allowing multiple tasks to progress simultaneously, leading to better efficiency.

Let’s delve into why concurrency is an essential aspect:

1. Improved Throughput and Efficiency

Concurrency allows multiple tasks to overlap, leading to enhanced system throughput. This capability enables a system to accomplish more work in the same amount of time.

For instance, an airline reservation system serves multiple customers at the same time. Each customer interacts with the system as if they have their own dedicated system. Behind the scenes, it’s concurrency that enables the system to handle multiple user requests simultaneously.

2. Better CPU Utilization

Concurrency improves CPU utilization by allowing a task to run whenever the CPU has free time, instead of waiting for one task to complete fully before starting another. The interleaving nature of concurrency ensures that the CPU is always kept busy, leading to efficient use of resources.

3. Concurrent Access for Multiple Users

Concurrency enables multiple users to access a shared resource simultaneously. For instance, a database management system allows multiple users to query and modify a database concurrently. Without concurrency, users would have to wait for their turn, leading to delays and dissatisfaction.

4. Real-Time Applications

Concurrency is essential for real-time systems where several tasks need to be executed simultaneously and independently. For instance, in a self-driving car, multiple operations like steering, monitoring the surroundings, and adjusting the speed need to happen concurrently to ensure smooth and safe driving.

In the end, understanding the importance of multithreading and concurrency is like understanding the importance of teamwork in a project. Just like a team combines different skills and resources to complete a project efficiently and effectively, multithreading and concurrency combine multiple threads and processes to enhance system performance and efficiency. With this understanding, we are one step closer to mastering the concepts of multithreading and concurrency. Let’s keep the ball rolling and dive deeper into these captivating topics in the next section.

Dive into Multithreading

Having grasped the basics and the significance of multithreading, let’s delve deeper into its fascinating world. Remember, understanding the nuances of multithreading will equip you to create efficient and robust applications.

A. Thread Lifecycle

Just like every living entity goes through a lifecycle, a thread in a computer program goes through various stages. Understanding this lifecycle is crucial to manage threads effectively. Let’s break down these stages:

1. New: The birth of a thread! This is when a thread is created but has not yet started running.

2. Runnable: The thread is ready to run and is waiting for its turn to be picked for execution by the thread scheduler based on thread priorities.

3. Running: The thread is currently being executed.

4. Blocked/Waiting: The thread is not currently eligible to run. It might be waiting for resources to become available or for certain tasks to complete.

5. Terminated: The thread has completed its job and has exited.

Visualize this cycle as a circular loop, and imagine a thread moving from one stage to another based on specific conditions. Understanding this helps in better thread management and thus, better performance.

B. Creating Threads in Different Languages

The concept of multithreading remains the same across programming languages, but the syntax and method of implementation may differ. Let’s consider how threads are created in some popular languages:

1. Java: In Java, we can create a thread by either implementing the Runnable interface or by extending the Thread class.

public class MyThread extends Thread {
public void run(){
System.out.println("Thread is running…");
}
}

In this example, MyThread is a user-defined class that extends the Thread class. The run() method contains the code that will be executed by the thread.

2. Python: Python uses the threading module to handle threads.

import threading

def worker():
print("Thread is running…")

t = threading.Thread(target=worker)
t.start()

Here, the worker function will run in a new thread when we call t.start().

3. C++: In C++, we can create a thread by constructing an object of the std::thread class and passing the function we want to execute in the thread as an argument.

#include <thread>
#include <iostream>

void worker() {
std::cout << "Thread is running…\n";
}

int main() {
std::thread t(worker);
t.join();
}

The function worker will be executed in a new thread.

Check Grokking the Coding Interview for a set of coding interview problems on multithreading.

C. Thread Synchronization

Now that we can create threads, we might ask, why do we need to synchronize them? To understand this, let’s consider a scenario where two threads are trying to access and modify the same data. The changes one thread makes could affect the behavior of the other thread, leading to unpredictable results. This is known as a race condition. To prevent this and ensure that our threads play nicely with each other, we use synchronization.

1. Locks: These are the most basic and essential synchronization technique. A lock allows only one thread to enter the part that’s locked, ensuring mutual exclusivity. Other threads attempting to enter the locked code will wait until the lock is released.

2. Semaphores: A semaphore restricts the number of simultaneous accesses to a shared resource. It’s like a nightclub with a certain capacity. If the capacity is reached, new people can enter only when someone inside leaves.

3. Monitors: A monitor is a synchronization construct that allows threads to have both mutual exclusion and the ability to wait for a certain condition to become true.

D. Thread Pooling

Creating and destroying threads requires a significant amount of CPU time and resources. To mitigate this, we use thread pools, a group of pre-instantiated, idle threads that stand ready to be used. This way, when a task arrives, it can be directly allocated to one of the idle threads, saving the overhead of thread creation.

In the end, threading is all about running numerous tasks concurrently, just like an efficient team working on different aspects of a project. Understanding the intricacies of multithreading enables us to manage threads effectively, thus creating high-performing, robust applications. Let’s continue our journey and delve into the world of concurrency in the next section.

Dive into Concurrency

Just like we plunged into the world of multithreading, it’s time we explore concurrency. While they are related, concurrency isn’t the same as multithreading. Concurrency is about dealing with a lot of things at once, whereas multithreading is one way to do multiple things at the same time.

A. Understanding Concurrency

Before we dive deeper, let’s first comprehend what concurrency means.

1. Concurrency Defined: Concurrency means multiple computations are happening in overlapping time periods. It’s like juggling. Even though you’re managing multiple balls, you’re only ever handling one at a time.

2. Key Elements of Concurrency: The key elements of concurrency are tasks, which represent an individual unit of work, and the creation, management, and coordination of these tasks. The way these tasks interact and share resources forms the crux of concurrency.

B. Concurrency vs. Parallelism

The terms concurrency and parallelism often get used interchangeably, but they denote different concepts.

1. Concurrency: As we’ve already discussed, concurrency is about dealing with multiple tasks at once. It doesn’t necessarily mean things happen simultaneously.

2. Parallelism: Parallelism, on the other hand, is about doing many things at the same time. In a parallel system, multiple tasks are executed at the same moment.

Picture this: A single-core CPU running multiple tasks by quickly switching between them is an example of concurrency. A multi-core processor running multiple tasks on different cores at the same time exemplifies parallelism.

C. Benefits of Concurrency

Let’s highlight the benefits that come along with the proper implementation of concurrency:

1. Improved Performance: Concurrency allows for the overlapping of input/output operations with computations, which can lead to significant performance gains, especially in I/O intensive programs.

2. Faster Response Time: In interactive systems, concurrency can make the system more responsive. While a long-running task is executing, the system can still respond to other inputs.

3. Better Resource Utilization: Concurrency enables better utilization of resources by allowing tasks to run whenever the resources they need are available.

D. Concurrency Models

There are various concurrency models, each offering different ways to structure tasks and their interactions. Let’s talk about a few popular ones:

1. Shared Memory: In this model, tasks interact by reading from and writing to shared memory locations. Synchronization primitives like locks are typically used to ensure that tasks do not interfere with each other.

2. Message Passing: In the message passing model, tasks communicate and synchronize by sending each other messages. There’s no shared state, and so, no need for locks.

3. Actor Model: The actor model is a type of message passing where “actors” (independent entities) communicate by sending messages. Each actor has a mailbox and processes messages sequentially, making this model inherently safe from the common concurrency issues.

E. Dealing with Concurrency Issues

Concurrency can lead to issues such as race conditions, deadlocks, and starvations if not properly managed. However, by understanding these problems and using proper synchronization techniques, we can mitigate these issues.

1. Race Conditions: Race conditions occur when the output is dependent on the sequence or timing of other uncontrollable events. It’s like two threads racing to write a value to a shared variable.

2. Deadlocks: Deadlocks occur when two or more tasks are unable to proceed because each is waiting for the other to release a resource.

3. Starvation: Starvation is a condition where a thread is unable to gain regular access to shared resources and is unable to make progress.

Understanding concurrency is like learning to juggle. It might seem intimidating initially, but once you get a grasp, it’s a skill that can significantly enhance the performance and responsiveness of your applications. As we move on to the next section, remember that mastering concurrency, like any other skill, comes with practice and patience. So let’s keep going!

Best Practices and Techniques

Now that we’ve explored the concepts of multithreading and concurrency, it’s time we discuss some best practices and techniques. These guidelines will help us design and implement effective multithreaded and concurrent applications.

A. Multithreading Best Practices

Let’s start with multithreading. Here are some tried-and-true strategies that you can follow:

1. Minimize Thread Usage: Threads are expensive to create and destroy, and too many threads can lead to a lot of context switching, reducing the overall efficiency of the system. So, use them sparingly and wisely.

2. Use Thread Pools: A thread pool is a group of pre-instantiated, idle threads that are ready to be used. Using a thread pool minimizes the overhead of thread creation and destruction.

3. Avoid Thread Priorities: Depending on thread priorities for program correctness can lead to portability issues, as thread scheduling is not consistent across different operating systems.

4. Synchronize Carefully: Incorrect synchronization can lead to problems like deadlocks and race conditions. It’s often better to use higher-level synchronization utilities provided by your language’s standard library, rather than trying to solve these problems yourself with low-level primitives.

B. Concurrency Best Practices

When it comes to concurrency, here are some best practices:

1. Understand Your Model: Different concurrency models have different advantages, disadvantages, and uses. Understand the one you are using and design your program accordingly.

2. Minimize Shared Mutable State: The more shared state, the harder it is to ensure threads don’t interfere with each other. Where possible, minimize the amount of shared state, especially shared mutable state.

3. Design for Failure: Concurrency errors can be hard to reproduce and diagnose. Therefore, try to isolate the effects of failure, so that when a failure occurs, it doesn’t bring down your whole system.

4. Test with Realistic Workloads: Concurrency-related bugs often only surface under load or in production. Make sure to test with realistic workloads and use tools to simulate different timings and orders of operations.

C. Techniques for Effective Multithreading and Concurrency

Now, let’s discuss some techniques that can help you manage threads and tasks more effectively:

1. Future and Promises: These constructs represent the result of a computation that may have not yet completed. They are an excellent way of managing asynchronous tasks.

2. Reactive Programming: This programming paradigm involves designing systems that respond to changes in input over time. Reactive Extensions (Rx) libraries exist for various languages and provide powerful abstractions for dealing with asynchronous streams of data.

3. Non-blocking Algorithms: These algorithms are designed to avoid unnecessary waiting and make better use of your system’s resources. They can be challenging to write correctly, but many languages provide libraries with non-blocking data structures and algorithms.

4. Transactional Memory: This technique simplifies concurrent code by allowing multiple memory operations to be performed in an atomic way. While not widely supported in all languages, where available, it can be a powerful tool.

5. Immutable Data Structures: Immutable data structures can’t be changed after they’re created. This makes them inherently safe to share between threads.

Understanding multithreading and concurrency isn’t just about memorizing concepts and definitions. It’s about recognizing patterns and learning to apply these techniques in the right way.

Conclusion

In modern computing, the ability to perform multiple tasks simultaneously is paramount. As we increasingly move towards multi-core and distributed computing, the importance of understanding multithreading and concurrency grows. They allow us to fully exploit the hardware capabilities of modern systems, leading to more efficient, responsive and faster applications.

Yet, this journey is not over. The world of multithreading and concurrency is vast, with much more to learn. For instance, there are many other models of concurrency to explore, such as data parallelism and task parallelism. There are also numerous other techniques, libraries, and tools designed to help manage threads and tasks, such as the C++ Concurrency API, Java’s Concurrency Utilities, and Python’s asyncio library.

Final Thoughts

Mastering multithreading and concurrency takes practice, patience, and a lot of hands-on experience. It’s about understanding the principles, recognizing the patterns, and knowing how and when to apply the various techniques and best practices.

It’s also about learning from our mistakes. Concurrency issues can be some of the most subtle and confusing problems to debug, but every bug is a learning opportunity. So, don’t be discouraged if you don’t get it right the first time.

Remember, in the world of software development, continuous learning is the key. As we continue to delve deeper into these concepts, we’ll keep uncovering new ways to build better, more efficient software.

On this note, let’s conclude our journey into the fascinating world of multithreading and concurrency. Let this blog serve as a launching pad for you to explore more, learn more, and implement more. Here’s to you building more efficient and robust applications that fully leverage the power of modern computing!

Happy coding, and until next time, keep threading!

➡ Check Grokking Multithreading and Concurrency for Coding Interviews for a list of coding problems on multi-threading.

➡ Check Grokking System Design Fundamentals for a list of common system design concepts.

➡ Learn more about these questions in “Grokking the System Design InterviewandGrokking the Advanced System Design Interview.”

➡ If you like this article, join my newsletter.

--

--

Founder www.designgurus.io | Formally a software engineer @ Facebook, Microsoft, Hulu, Formulatrix | Entrepreneur, Software Engineer, Writer.