In computer science, concurrency is the ability of different parts or units of a program, algorithm, or problem to be executed out-of-order or in partial order, without affecting the final outcome. Here is a list of coding interview questions on Concurrency to help you get ready for your next data structures interview in 2021.
- 1.
What is a Deadlock?
Answer:
Deadlock is a state of a computer system where two or more processes are unable to proceed because each is waiting for one or more others to release a resource. It is a type of livelock where multiple processes or threads are caught in a circular chain of needing resources that they cannot release.
Core Characteristics
-
Mutual Exclusion: At least one resource must be held exclusively (non-sharable) in a manner that if one process is using it, then another process must wait for the first one to release the resource.
-
Hold and Wait: Processes already holding resources can request additional resources and are waiting for them to be satisfied.
-
No Preemption: Resources cannot be forcibly removed from a process.
-
Circular Wait: There is a chain of processes wherein each process is holding a resource that is requested by the next process in the chain.
Code Example: Simulating Deadlock in Python
In the following Python code, two threads are trying to acquire two locks in opposite order. This creates a situation where both threads are holding one lock each and waiting for the lock held by the other thread.
import threading # Resource A and B represented by Lock objects lock_a = threading.Lock() lock_b = threading.Lock() # Function executed by Thread 1 def thread_1(): lock_a.acquire() lock_b.acquire() print("Thread 1: Acquired both locks") lock_b.release() lock_a.release() # Function executed by Thread 2 def thread_2(): lock_b.acquire() lock_a.acquire() # Causes the deadlock print("Thread 2: Acquired both locks") lock_a.release() lock_b.release() # Create and start both threads t1 = threading.Thread(target=thread_1) t2 = threading.Thread(target=thread_2) t1.start() t2.start() t1.join() t2.join()
This code will result in a deadlock, where both threads are indefinitely waiting for the lock held by the other thread, thus unable to proceed.
Strategies for Deadlock Prevention and Recovery
Deadlock Prevention Strategies
-
Mutual Exclusion Avoidance: Ensure that resources can be shared instead of being held exclusively.
-
Hold and Wait Avoidance: Acquire all resources a process will need before it starts execution.
-
No Preemption Strategy: Allow resources to be preempted from processes.
-
Circular Wait Avoidance: Order resources and request them in the same order.
Deadlock Recovery Strategies
-
Process Termination: Abort one or more processes involved in the deadlock.
-
Resource Preemption: Preempt resources from one or more processes and give them to others.
-
Rollback: Rollback the actions of one or more processes to a previous checkpoint and restart them.
Deadlock in Real-World Scenarios
-
Database Management: When a transaction holds a lock on a data item and tries to lock another item, it may go into a deadlock condition.
-
Interlocking Train Tracks: If two trains are moving toward each other on different tracks and there is a switch malfunction, a deadlock can occur.
-
- 2.
Explain the difference between Asynchronous and Parallel programming?
Answer:
Both Asynchronous and Parallel programming aim to improve program execution, but they do so in different ways and for different purposes.
- Asynchronous programming: Primarily used to manage the efficiency of a single task by allowing it to "yield" to other tasks, without blocking the main execution thread until it completes.
- Parallel programming: Used to perform multiple tasks simultaneously, with the goal of utilizing multiple computing resources to speed up the overall process.
Key Differences
Execution Context
- Asynchronous Programming:
- Within a single thread.
- Uses features like event loops and callbacks.
- Parallel Programming:
- Can utilize multiple threads, processes, or machines.
- Often managed through specialized libraries or APIs, like
concurrent.futures
in Python.
Resource Utilization
- Asynchronous Programming:
- Well-suited for tasks that are I/O-intensive, like network operations or file downloads.
- Allows the CPU to work on other tasks while waiting for I/O to complete.
- Parallel Programming:
- Benefits CPU-bound tasks, like complex mathematical computations.
- Can improve speed by maximizing CPU resources.
Complexity
- Asynchronous Programming:
- Often requires complex callback chains or the use of more modern, easy-to-use constructs like Python's
asyncio
.
- Often requires complex callback chains or the use of more modern, easy-to-use constructs like Python's
- Parallel Programming:
- More complex to implement, especially when dealing with shared resources or race conditions.
Real-World Examples
Asynchronous Programming
- Web Servers: Asynchronous server frameworks like
asgi
in Python are often used to serve web requests efficiently. - Web Scraping: Async libraries like
aiohttp
in Python can be used to scrape websites in a non-blocking manner.
Parallel Programming
- Machine Learning: Libraries like
TensorFlow
can utilize multiple GPUs for faster model training. - Data Processing Pipelines: Tools like
Apache Spark
distribute computational tasks across a cluster of machines for faster data processing.
Code Example: Asynchronous Programming in Python
Here's a simple Python code snippet that demonstrates the concept of asynchronous programming using Python's
asyncio
library:import asyncio async def perform_task(task_name): print(f'Starting task: {task_name}') await asyncio.sleep(1) print(f'Completed task: {task_name}') # The event loop async def main(): tasks = [ asyncio.ensure_future(perform_task('Task 1')), asyncio.ensure_future(perform_task('Task 2')), ] await asyncio.gather(*tasks) # Run the main program if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main())
In this example, the functions
perform_task()
are tasks that are executed asynchronously in themain()
function usingasyncio
. Each task simulates work by sleeping for 1 second. - 3.
What is a Mutex?
Answer:
A Mutex (short for mutual exclusion) is a synchronization primitive commonly used in multi-threaded and multi-process applications to prevent more than one thread or process from entering a critical section of code (a user-defined portion of code that should not run concurrently by multiple threads or processes for data integrity or consistency reasons). It is a special type of lock that can be acquired and owned by only one thread at a time. When a thread acquires a Mutex, other threads attempting to acquire it will be blocked until the Mutex is released by the owning thread.
Why Do We Need Mutexes?
In a multi-threaded or multi-process environment, race conditions can occur when two or more threads or processes access shared data or resources simultaneously, and at least one of them tries to modify it. This can lead to data corruption, inconsistency, and other bugs that are hard to debug, detect, and fix.
Mutexes serve as a guard for these critical sections, ensuring that only one thread executes a particular piece of code at a time, thereby avoiding race conditions and maintaining data integrity.
Features of Mutex
- Mutual Exclusion: Ensures that only one thread or process can own the Mutex at any given time.
- Ownership: The thread that has acquired the Mutex is the only one that can release it.
- Blocking: If a second thread tries to acquire a Mutex that is already locked, it will be blocked until the Mutex is released.
How Does a Mutex Work?
When a thread wants to enter a critical section, it tries to acquire the Mutex. There are generally two possible scenarios:
- If the Mutex is unlocked, meaning no other thread owns it, the thread that wants to enter the critical section acquires the Mutex and proceeds with its work.
- If the Mutex is locked, the thread that wants to acquire it will be blocked (i.e., it will be made to wait) until it can obtain ownership.
Once the thread that owns the Mutex has completed its work in the critical section, it releases the Mutex, thereby making it available for other threads to acquire.
Mutex Implementations
The mechanics of Mutex implementation can vary across programming languages and operating systems, but the core idea remains the same: to allow exclusive access to a resource. Some common Mutex types are:
- Binary Mutex: Can be in two states only; locked or unlocked.
- Counting Mutex: Can have a count greater than one, allowing nested acquisitions.
- Recursive Mutex: Special type of counting Mutex that allows the same thread to acquire it multiple times.
Mutex vs. Semaphore
A Mutex is often compared to a Semaphore, another popular synchronization primitive. While both serve to control access to a shared resource, the primary difference lies in their capabilities:
- Mutual Exclusion vs. Resource Synchronization: Mutexes are typically used to serialize concurrent access to a shared resource, while Semaphores can also be used for more complex synchronization patterns.
- Binary vs. Counting: Mutexes are generally binary, whereas Semaphores can maintain a count and allow multiple threads to enter a critical section.