Multithreading and Multiprocessing in Python - Interview Questions and Answers
Multithreading uses multiple threads within the same process to execute tasks concurrently, while multiprocessing creates separate processes, each with its own memory space. Multiprocessing avoids the Global Interpreter Lock (GIL), making it better for CPU-bound tasks.
The GIL is a mechanism in CPython that ensures only one thread executes Python bytecode at a time. This means Python threads cannot fully utilize multi-core processors for CPU-bound tasks.
Since the GIL restricts threads from running in parallel for CPU-bound tasks, multiprocessing is preferred when you need true parallel execution.
The threading module in Python provides support for creating and managing threads.
The multiprocessing module provides support for process-based parallelism.
import threading
def print_message():
print("Hello from thread")
t = threading.Thread(target=print_message)
t.start()
t.join()
import multiprocessing
def print_message():
print("Hello from process")
p = multiprocessing.Process(target=print_message)
p.start()
p.join()
import threading
def greet(name):
print(f"Hello, {name}")
t = threading.Thread(target=greet, args=("Alice",))
t.start()
t.join()
import multiprocessing
def greet(name):
print(f"Hello, {name}")
p = multiprocessing.Process(target=greet, args=("Alice",))
p.start()
p.join()
import threading
import time
def task():
print("Starting task")
time.sleep(2)
print("Task completed")
t = threading.Thread(target=task)
t.start()
t.join()
t.is_alive() # Returns True if the thread is still running
Use .join() on the thread to make the main program wait for it to finish.
import threading
def task():
print(f"Thread name: {threading.current_thread().name}")
t = threading.Thread(target=task, name="WorkerThread")
t.start()
t.join()
Thread runs in the same memory space, while Process runs in a separate memory space, making multiprocessing better for CPU-intensive tasks.
Use t.daemon = True before starting the thread.
Daemon threads run in the background and exit automatically when the main program exits.
Use shared objects like lists, dictionaries, or synchronization mechanisms like threading.Lock.
Use multiprocessing.Queue, multiprocessing.Value, or multiprocessing.Array.
A Lock is used to prevent multiple threads from accessing shared resources at the same time.
import threading
lock = threading.Lock()
def task():
with lock:
print("Thread is running with lock")
t1 = threading.Thread(target=task)
t2 = threading.Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
A Semaphore is a synchronization primitive that limits the number of threads that can access a resource at the same time.
import threading
semaphore = threading.Semaphore(2)
def task():
with semaphore:
print(f"Thread {threading.current_thread().name} is running")
import time
time.sleep(2)
threads = [threading.Thread(target=task) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
An Event is a flag that can be set or cleared to control thread execution.
import threading
event = threading.Event()
def wait_for_event():
print("Thread waiting for event...")
event.wait()
print("Event received, thread running!")
t = threading.Thread(target=wait_for_event)
t.start()
import time
time.sleep(2)
event.set()
t.join()
A Barrier allows multiple threads to synchronize at a common point before proceeding.
import threading
barrier = threading.Barrier(3)
def task():
print(f"{threading.current_thread().name} waiting at barrier")
barrier.wait()
print(f"{threading.current_thread().name} passed barrier")
threads = [threading.Thread(target=task) for _ in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
threading.Queueis used for inter-thread communication.multiprocessing.Queueis used for inter-process communication.
Use queue.Queue for thread-safe operations.
import queue
import threading
q = queue.Queue()
def worker():
while not q.empty():
item = q.get()
print(f"Processing {item}")
q.task_done()
for i in range(5):
q.put(i)
threads = [threading.Thread(target=worker) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
Python does not provide a direct way to terminate threads. Use flags or threading.Event.
It creates thread-local storage, allowing data to be unique per thread.
It allows parallel execution of a function using multiple processes.
import multiprocessing
def square(n):
return n * n
with multiprocessing.Pool(4) as pool:
result = pool.map(square, [1, 2, 3, 4, 5])
print(result)
from multiprocessing import Manager, Process
def worker(shared_list):
shared_list.append("Hello from process")
with Manager() as manager:
shared_list = manager.list()
p = Process(target=worker, args=(shared_list,))
p.start()
p.join()
print(shared_list)
apply()runs a function in one process and returns the result.apply_async()runs a function asynchronously.map()applies a function to all elements of an iterable in parallel.map_async()does the same but asynchronously.
Value allows sharing a single value between processes, while Array allows sharing a list.
Use multiprocessing.Queue, which is process-safe by default.
It prevents multiple processes from accessing a shared resource simultaneously.
import multiprocessing
lock = multiprocessing.Lock()
def task():
with lock:
print("Process is running with lock")
p1 = multiprocessing.Process(target=task)
p2 = multiprocessing.Process(target=task)
p1.start()
p2.start()
p1.join()
p2.join()
It allows a process to wait until another process meets a certain condition.
ThreadPoolExecutor is used for I/O-bound tasks, while ProcessPoolExecutor is used for CPU-bound tasks.
from concurrent.futures import ProcessPoolExecutor
def square(n):
return n * n
with ProcessPoolExecutor() as executor:
results = executor.map(square, [1, 2, 3, 4, 5])
print(list(results))
Use synchronization primitives like Lock, Semaphore, or Queue.
os.fork()is Unix-specific and creates a child process by duplicating the parent process.multiprocessing.Processis cross-platform and starts a new process.
No, due to the GIL, threads execute one at a time. Only multiprocessing achieves true parallel execution.
- Deadlocks due to improper locking.
- Pickling errors when passing non-picklable objects.
- Resource leaks if processes are not properly closed.
A daemon thread runs in the background and terminates when all non-daemon threads finish.
import threading
import time
def background_task():
while True:
print("Daemon thread running...")
time.sleep(2)
thread = threading.Thread(target=background_task, daemon=True)
thread.start()
time.sleep(5)
print("Main thread exiting")Here, the daemon thread stops when the main thread exits.
Use the join(timeout) method.
import threading
def task():
import time
time.sleep(10)
thread = threading.Thread(target=task)
thread.start()
thread.join(timeout=5) # Waits for 5 seconds
if thread.is_alive():
print("Thread is still running, timeout occurred")
A worker thread executes tasks asynchronously in a pool, improving performance in I/O-bound applications.
Use ThreadPoolExecutor from concurrent.futures.
from concurrent.futures import ThreadPoolExecutor
def worker(n):
return n * n
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(worker, [1, 2, 3, 4, 5])
print(list(results))
Pipe allows bidirectional communication between two processes, while Queue supports multiple producers and consumers.
from multiprocessing import Pipe, Process
def sender(conn):
conn.send("Hello from child process")
conn.close()
parent_conn, child_conn = Pipe()
p = Process(target=sender, args=(child_conn,))
p.start()
print(parent_conn.recv()) # Receiving data
p.join()
Catch exceptions inside the thread and use a shared variable to store errors.
- Zombie process: A child process that finishes execution but still has an entry in the process table.
- Orphan process: A child process whose parent has terminated.
- Use timeouts when acquiring locks.
- Use thread-safe data structures like
queue.Queue(). - Avoid circular wait conditions.
- CPU-bound: Use
multiprocessingto utilize multiple cores. - I/O-bound: Use
threadingorasynciosince the GIL allows I/O tasks to run concurrently.
Use cProfile for performance profiling.
import cProfile
import threading
def task():
sum(range(100000))
thread = threading.Thread(target=task)
cProfile.run("thread.start(); thread.join()")
Process affinity binds a process to specific CPU cores. Use psutil to control it.
Use the terminate() method in multiprocessing.Process.
from multiprocessing import Process
import time
def task():
while True:
print("Running...")
time.sleep(1)
p = Process(target=task)
p.start()
time.sleep(5)
p.terminate()
p.join()
print("Process terminated")
Use multiprocessing.shared_memory to avoid unnecessary copying.
It occurs when low-priority threads are indefinitely delayed by high-priority threads. Use fair scheduling or adjust thread priorities.
Use Semaphore in threading or BoundedSemaphore in multiprocessing.
Yes, but avoid if __name__ == "__main__" issues by placing multiprocessing code inside functions.
- Windows: Uses
spawn(creates a fresh process). - Linux: Uses
fork(copies the parent process).
It yields the CPU to another thread or process, useful for cooperative multitasking.
asynciois best for single-threaded concurrency using coroutines.threadingis best for I/O-bound tasks.multiprocessingis best for CPU-bound tasks.
- Use logging instead of
print(). - Use threading.enumerate() to track active threads.
- Use GDB with Python extensions for deep debugging.
Barrier synchronizes multiple threads, making them wait at a checkpoint before proceeding.
import threading
barrier = threading.Barrier(3)
def task(n):
print(f"Thread-{n} waiting at barrier")
barrier.wait()
print(f"Thread-{n} passed the barrier")
for i in range(3):
threading.Thread(target=task, args=(i,)).start()
Use threading.enumerate() to list all active threads.
import threading
def worker():
import time
time.sleep(2)
t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=worker)
t1.start()
t2.start()
print(threading.enumerate()) # Lists all running threads
Use the logging module with ThreadingHandler or QueueHandler.
It provides shared objects like lists and dictionaries across processes.
from multiprocessing import Manager, Process
def worker(shared_list):
shared_list.append(1)
with Manager() as manager:
shared_list = manager.list()
processes = [Process(target=worker, args=(shared_list,)) for _ in range(3)]
for p in processes:
p.start()
for p in processes:
p.join()
print(shared_list) # Output: [1, 1, 1]
Python does not automatically resolve deadlocks. Use timeouts, lock hierarchy, or try-except blocks.
Array allows sharing data between processes without using a Manager.
from multiprocessing import Array, Process
def worker(arr):
arr[0] = 99
shared_array = Array('i', [1, 2, 3])
p = Process(target=worker, args=(shared_array,))
p.start()
p.join()
print(shared_array[:]) # Output: [99, 2, 3]
JoinableQueue extends Queue with a task_done() method for better synchronization.
It uses a queue where tasks with higher priority are processed first.
No, Python only allows one running event loop per thread.
GIL prevents true parallel execution of Python threads, limiting performance in CPU-bound tasks.
Use multiprocessing, cython, numba, or JIT-optimized code.
os.fork()creates a process by duplicating the current one (Linux only).multiprocessing.Process()creates a new process that starts from the function entry.
Yes, using libraries like Ray or Dask.
Use concurrent.futures.ProcessPoolExecutor.
subprocessis for running external programs.multiprocessingis for parallel execution of Python code.
Tutorials
Random Blogs
- Avoiding the Beginner’s Trap: Key Python Fundamentals You Shouldn't Skip
- Mastering SQL in 2025: A Complete Roadmap for Beginners
- AI & Space Exploration – AI’s Role in Deep Space Missions and Planetary Research
- What is YII? and How to Install it?
- Ideas for Content of Every niche on Reader’s Demand during COVID-19
- Store Data Into CSV File Using Python Tkinter GUI Library
- Time Series Analysis on Air Passenger Data
- The Ultimate Guide to Artificial Intelligence (AI) for Beginners
- Datasets for Natural Language Processing
- Types of Numbers in Python
