Logo OnlineCBT
PYTHON Tutorial
PYTHON · LESSON 45

Threading and Multiprocessing

Choose concurrency models for I/O-bound and CPU-bound tasks.

Level: Beginner to Advanced
Duration: ~60 Mins Deep-Dive
Updated: 26 Jul 2026

Lesson 45: Threading and Multiprocessing

1. Introduction to Concurrent and Parallel Execution

Welcome to Module 7 of our Python Mastery Series: Concurrent, Parallel and Asynchronous Execution! Up to this point in our journey, all Python scripts we constructed executed sequentially in a single synchronous thread. In sequential execution, Python processes instructions line by line—if a function pauses for 5 seconds waiting for a network HTTP response or a file write, the entire CPU core sits completely idle, blocking all subsequent tasks.

Modern hardware infrastructure consists of multi-core processors capable of running multiple execution streams simultaneously. To maximize system throughput, reduce API latency, and scale enterprise workloads, software engineers must master Concurrent and Parallel Programming.

In this lesson, we will explore the fundamental differences between Concurrency and Parallelism, demystify CPython's Global Interpreter Lock (GIL), differentiate I/O-Bound vs CPU-Bound workloads, master the standard `threading` and `multiprocessing` modules, utilize high-level Worker Pools via `concurrent.futures`, and route concurrent worker result streams using Structural Pattern Matching (`match-case`).


2. Architectural Foundations: Concurrency vs Parallelism

1. Concurrency (Interleaved Execution)

Concurrency is about dealing with lots of things at once. It manages multiple execution tasks by overlapping their progress on a single core through rapid context switching. While task A waits for disk I/O, the CPU switches context to process task B.

2. Parallelism (Simultaneous Execution)

Parallelism is about doing lots of things at once. It executes multiple tasks simultaneously across physically separate CPU hardware cores.

  • Execution Paradigm
  • Concurrent (Single Process, Shared Memory)
  • Parallel (Multiple Processes, Isolated Memory)
  • Target Task Category
  • I/O-Bound Workloads
    (Network requests, File I/O, DB queries)
  • CPU-Bound Workloads
    (Heavy math, image processing, cryptography)
  • Memory Footprint
  • Lightweight (Threads share address space)
  • Heavier (Each process duplicates Python interpreter)
  • GIL Impact Status
  • Constrained by CPython's GIL
  • Bypasses GIL entirely!
  • Primary Safety Hazard
  • Race conditions on shared mutable data
  • Inter-Process Communication (IPC) serialization overhead
Architectural Dimension Multithreading (`threading`) Multiprocessing (`multiprocessing`)

3. Demystifying CPython's Global Interpreter Lock (GIL)

The Global Interpreter Lock (GIL) is a mutual-exclusion lock used by the standard CPython interpreter to prevent multiple native OS threads from executing Python bytecodes simultaneously.

Why the GIL Exists: CPython's internal memory management uses reference counting. Without the GIL, concurrent threads mutating Python object reference counts simultaneously would trigger catastrophic memory corruption bugs and race conditions inside the CPython interpreter core!

1. Practical Impact of the GIL on Performance

  • For I/O-Bound Tasks (`threading`): Threads release the GIL automatically whenever they wait for external network I/O, disk writes, or database sockets. Therefore, multithreading delivers massive concurrency speedups for I/O tasks!
  • For CPU-Bound Tasks (`multiprocessing`): Threads compute math continuously without releasing the GIL. Consequently, multithreading multiple CPU tasks on CPython runs sequentially (and often slower due to lock context switching!). To achieve true multi-core CPU parallelism, you MUST spawn separate Operating System processes using multiprocessing!

4. Multithreading for I/O-Bound Workloads (`threading` Module)

The threading module allows spawning multiple OS threads within a single Python process. Because threads share the exact same memory space, synchronizing access to shared mutable variables using a Thread Lock (`threading.Lock`) is required to prevent race conditions.

THREADING_LOCK_DEMO.PY
import threading
import time

# Shared mutable state resource
shared_bank_balance = 1000.0
balance_lock = threading.Lock()  # Mutex Lock preventing race conditions

def deposit_worker(thread_id: str, amount: float):
    global shared_bank_balance
    print(f"[{thread_id}] Initiating secure deposit of ${amount:.2f}...")
    
    # Simulating I/O network latency (releases GIL during sleep!)
    time.sleep(0.1)
    
    # Guarding shared state access via Thread Lock context manager
    with balance_lock:
        current = shared_bank_balance
        time.sleep(0.05)  # Simulating processing
        shared_bank_balance = current + amount
        print(f"[{thread_id}] SUCCESS: Balance updated -> ${shared_bank_balance:.2f}")

# Spawning multiple concurrent threads
t1 = threading.Thread(target=deposit_worker, args=("Thread-1", 200.0))
t2 = threading.Thread(target=deposit_worker, args=("Thread-2", 300.0))

print("=== Starting Concurrent Threads ===")
t1.start()
t2.start()

# Waiting for both threads to complete execution via .join()
t1.join()
t2.join()

print(f"\nFinal Synchronized Balance: ${shared_bank_balance:.2f}")
OUTPUT
=== Starting Concurrent Threads ===
[Thread-1] Initiating secure deposit of $200.00...
[Thread-2] Initiating secure deposit of $300.00...
[Thread-1] SUCCESS: Balance updated -> $1200.00
[Thread-2] SUCCESS: Balance updated -> $1500.00

Final Synchronized Balance: $1500.00

5. Multiprocessing for CPU-Bound Workloads (`multiprocessing` Module)

To bypass CPython's GIL and achieve true multi-core parallel computation, use the multiprocessing module. Spawning individual OS processes allocates separate Python interpreter instances, separate memory spaces, and isolated GIL instances across physical CPU cores.

The Entry Point Guard Requirement: Scripts spawning subprocesses using multiprocessing MUST wrap process execution inside an if __name__ == "__main__": block! Omitting this guard causes infinite subprocess spawning loops on Windows and macOS, crashing the Operating System!
MULTIPROCESSING_BASIC_DEMO.PY
import multiprocessing
import time

def compute_heavy_factorials(task_id: int, number: int) -> tuple[int, int]:
    """CPU-Bound heavy calculation function executing on isolated process core."""
    print(f"[PROCESS-{task_id}] Computing factorial of {number} on Core PID #{multiprocessing.current_process().pid}...")
    start_t = time.perf_counter()
    
    result = 1
    for i in range(1, number + 1):
        result *= i
        
    duration = time.perf_counter() - start_t
    print(f"[PROCESS-{task_id}] Completed in {duration:.4f}s.")
    return (task_id, result)

if __name__ == "__main__":
    print("=== Spawning Isolated Multiprocessing Workers ===")
    
    # Spawning 2 independent OS processes
    p1 = multiprocessing.Process(target=compute_heavy_factorials, args=(1, 50000))
    p2 = multiprocessing.Process(target=compute_heavy_factorials, args=(2, 50000))
    
    p1.start()
    p2.start()
    
    p1.join()
    p2.join()
    print("All parallel CPU worker processes completed successfully.")
OUTPUT
=== Spawning Isolated Multiprocessing Workers ===
[PROCESS-1] Computing factorial of 50000 on Core PID #14023...
[PROCESS-2] Computing factorial of 50000 on Core PID #14024...
[PROCESS-1] Completed in 0.1250s.
[PROCESS-2] Completed in 0.1265s.
All parallel CPU worker processes completed successfully.

6. High-Level Executor Pools: `concurrent.futures`

Manually managing thread and process lifecycles (calling .start(), .join(), managing locks) quickly becomes cumbersome. The modern standard library's `concurrent.futures` module provides a clean, unified high-level abstraction layer using Worker Pools.

1. Executor Pool Selection Guide

  • ThreadPoolExecutor: Manages a pool of concurrent threads. Ideal for **I/O-Bound tasks** (HTTP downloads, file reads).
  • ProcessPoolExecutor: Manages a pool of parallel OS processes. Ideal for **CPU-Bound tasks** (data analysis, mathematical calculations).
EXECUTOR_POOLS_DEMO.PY
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def simulate_http_fetch(url: str) -> tuple[str, int, float]:
    """Simulates fetching external web page over network."""
    start = time.perf_counter()
    time.sleep(0.1)  # Simulating network latency
    elapsed = time.perf_counter() - start
    return (url, 200, elapsed)

urls_to_fetch = [
    "https://api.gateway.com/v1/users",
    "https://api.gateway.com/v1/orders",
    "https://api.gateway.com/v1/metrics",
    "https://api.gateway.com/v1/audit"
]

print("=== Executing Parallel Fetch via ThreadPoolExecutor ===")
# Automatically manages thread creation and teardown!
with ThreadPoolExecutor(max_workers=4) as executor:
    # Submitting asynchronous task batch using executor.map() or submit()
    futures = [executor.submit(simulate_http_fetch, url) for url in urls_to_fetch]
    
    # Consuming completed worker tasks as they finish via as_completed()
    for future in as_completed(futures):
        url, status, duration = future.result()
        print(f"Fetched: {url:<35} | Status: {status} | Latency: {duration:.4f}s")
OUTPUT
=== Executing Parallel Fetch via ThreadPoolExecutor ===
Fetched: https://api.gateway.com/v1/users    | Status: 200 | Latency: 0.1012s
Fetched: https://api.gateway.com/v1/orders   | Status: 200 | Latency: 0.1015s
Fetched: https://api.gateway.com/v1/metrics  | Status: 200 | Latency: 0.1018s
Fetched: https://api.gateway.com/v1/audit    | Status: 200 | Latency: 0.1021s

7. Combining Concurrent Results with `match-case` Pattern Matching

Building scalable concurrent task dispatchers involves submitting asynchronous jobs to worker pools and using match-case structural pattern matching to parse completed worker tuple result schemas dynamically.

MATCH_CASE_CONCURRENCY_DISPATCHER.PY
from concurrent.futures import ThreadPoolExecutor, as_completed

def execute_worker_task(task_payload: tuple) -> tuple:
    """Worker task processing various operation types."""
    match task_payload:
        case ("DB_QUERY", sql_query):
            return ("SUCCESS", "DB", f"Executed: '{sql_query}'", 120)
            
        case ("HTTP_CALL", url_endpoint) if "fail" in url_endpoint:
            return ("ERROR", "HTTP", f"Endpoint '{url_endpoint}' unreachable!", 500)
            
        case ("HTTP_CALL", url_endpoint):
            return ("SUCCESS", "HTTP", f"Payload fetched from '{url_endpoint}'", 200)
            
        case _:
            return ("UNKNOWN", "SYSTEM", f"Invalid payload schema: {task_payload}", 400)

if __name__ == "__main__":
    task_queue = [
        ("DB_QUERY", "SELECT * FROM inventory;"),
        ("HTTP_CALL", "https://api.corp.com/v1/status"),
        ("HTTP_CALL", "https://api.corp.com/v1/fail_check"),
        ("INVALID_CMD",)
    ]

    print("=== Processing Concurrent Task Pool via Pattern Dispatcher ===")
    with ThreadPoolExecutor(max_workers=4) as executor:
        futures = [executor.submit(execute_worker_task, task) for task in task_queue]
        
        for future in as_completed(futures):
            result_tuple = future.result()
            
            # Matching task result schema dynamically
            match result_tuple:
                case ("SUCCESS", service_type, message, status_code):
                    print(f"[OK 200] [{service_type}] {message}")
                    
                case ("ERROR", service_type, error_msg, err_code):
                    print(f"[FAIL {err_code}] [{service_type}] {error_msg}")
                    
                case _:
                    print(f"[REJECTED] Unrecognized result schema: {result_tuple}")
OUTPUT
=== Processing Concurrent Task Pool via Pattern Dispatcher ===
[OK 200] [DB] Executed: 'SELECT * FROM inventory;'
[OK 200] [HTTP] Payload fetched from 'https://api.corp.com/v1/status'
[FAIL 500] [HTTP] Endpoint 'https://api.corp.com/v1/fail_check' unreachable!
[REJECTED] Unrecognized result schema: ('UNKNOWN', 'SYSTEM', "Invalid payload schema: ('INVALID_CMD',)", 400)

8. Frequently Asked Interview Questions with Answers

Q1: What is CPython's Global Interpreter Lock (GIL), and why does it affect multithreaded performance?
Answer: The GIL is a mutex lock in CPython that restricts thread execution to a single native thread at a time per process. It prevents true CPU parallel execution across multiple cores in Python multithreading. However, because threads release the GIL during I/O wait states, multithreading remains highly effective for I/O-bound tasks.
Q2: When should you use `threading` vs `multiprocessing` in Python?
Answer: Use `threading` for **I/O-Bound tasks** (file I/O, network requests, database queries) where threads spend time waiting for external I/O. Use `multiprocessing` for **CPU-Bound tasks** (heavy math, data compression, image manipulation) to bypass the GIL and execute in parallel across separate physical CPU cores.
Q3: What is a Race Condition, and how do you prevent it in Python multithreading?
Answer: A Race Condition occurs when multiple concurrent threads attempt to read and mutate shared memory variables simultaneously, leading to unpredictable data corruption. Race conditions are prevented by synchronizing access to critical code sections using a Mutex Lock (threading.Lock()).
Q4: What is the advantage of using `concurrent.futures` over raw `threading.Thread` or `multiprocessing.Process`?
Answer: concurrent.futures provides a high-level Executor Pool abstraction (ThreadPoolExecutor & ProcessPoolExecutor). It automatically manages worker allocation, task queuing, exception propagation, and asynchronous result gathering via Future objects without requiring manual thread joins or lock setup.
Q5: Why is `if __name__ == "__main__":` mandatory when using `multiprocessing`?
Answer: On platforms using the spawn process start method (Windows and macOS), new child processes import the main module to initialize interpreter state. Without the entry point guard, child processes re-execute module-level process creation code recursively, triggering an infinite process creation loop that crashes the OS.

9. Homework & Practical Assignments

Task 1: Concurrent File Downloader with ThreadPoolExecutor

Create a script named concurrent_downloader_task.py inside your lesson_45 folder:

  • Import ThreadPoolExecutor from concurrent.futures and time.
  • Define function download_file(file_id: int) -> tuple[int, str] simulating network latency using time.sleep(0.2).
  • Submit a batch of 10 file download tasks into a ThreadPoolExecutor(max_workers=5).
  • Iterate through completed tasks using as_completed() and print formatted completion statements using f-strings.

Task 2: Multiprocessing CPU Benchmark with Pattern Matcher

Create a script named parallel_math_task.py:

  • Import ProcessPoolExecutor from concurrent.futures.
  • Define CPU function sum_of_squares(n: int) -> tuple[str, int, int].
  • Write a match-case dispatcher processing worker return tuples:
    • case ("SUCCESS", limit, total) → Output formatted calculation result.
    • case ("ERROR", _, msg) → Output calculation error.
    • case _ → Output unknown status.
  • Wrap execution inside if __name__ == "__main__": and execute calculation across 4 process workers.

Task 3: Master Review Capstone Project — Production Enterprise Multi-Core Concurrent Task Engine (`concurrency_engine_os.py`)

Create a script named concurrency_engine_os.py inside your lesson_45 folder. This assignment tests and integrates **ALL concepts learned across Lessons 1 through 45**.

Project Architectural Requirements Specification:

  1. Threading & Multiprocessing Architecture (Lesson 45):
    • Implement concurrent I/O task worker pools using ThreadPoolExecutor with threading.Lock synchronization.
    • Implement parallel CPU math worker pools using ProcessPoolExecutor to bypass CPython's GIL.
    • Wrap execution safely inside an if __name__ == "__main__": entry point guard.
  2. Temporal & RegEx Text Processing (Lessons 43-44):
    • Parse UTC-aware event timestamps using datetime and zoneinfo.ZoneInfo.
    • Pre-compile domain RegEx patterns with Named Groups for extracting task IDs from log strings.
  3. Type Hints, Generics & Static Safety (Lesson 42):
    • Apply explicit modern type annotations (X | Y, list[str], dict[str, Any]) throughout all functions and classes.
    • Define domain TypeAlias definitions and Generic Response Envelopes.
  4. Metaprogramming, Closures & Decorators (Lessons 40-41):
    • Build function factories generating customized metric accumulator closures.
    • Build a 3-level configurable decorator audit_concurrency_execution(log_level="INFO") with @functools.wraps.
  5. Iterators, Generators & Functional Tools (Lesson 39):
    • Implement generator functions streaming worker job queues lazily with $O(1)$ memory footprint.
  6. Full OOP Architecture (Lessons 32-38):
    • Define abstract base class BaseTaskWorker(ABC) and concrete Data Classes ConcurrentTask and WorkerResult.
    • Build composite manager ConcurrencyEngineOS composing storage drivers and custom context managers.
  7. Observability & Fault Tolerance (Lessons 29-31):
    • Set up multi-handler logging to console and concurrency_os.log file.
    • Incorporate developer assertions (assert) verifying worker pool limits.
    • Define custom exception hierarchy (ConcurrencyEngineError, WorkerTimeoutError).
  8. Context Managers & Persistence Layer (Lessons 27-30):
    • Use custom class-based context manager TaskVaultLock to manage persistent database lock files.
    • Persist JSON records to concurrent_db.json and export CSV audit reports to concurrency_audit.csv using pathlib.Path.
  9. Nested Collections & Comprehensions (Lessons 21-26):
    • Maintain in-memory registry dictionaries mapping Task IDs to Data Class Instances.
    • Use List and Dictionary Comprehensions to clean and transform records.
  10. Advanced Function Parameters & Scope (Lessons 15-17):
    • Structure logic into pure modular functions with type hints, docstrings, early return Guard Clauses, and *args / **kwargs logging.
  11. Indefinite & Definite Loops (Lessons 13-14):
    • Run the interactive CLI interface inside a continuous while True menu loop.
    • Iterate through output reports using for loops with enumerate() and .items().
  12. Pattern Matching CLI Dispatcher (Lesson 12):
    • Process user CLI command tokens inside a match-case block with concurrency match guards:
      • case ["SUBMIT", "IO", task_id, target_url] → Submit I/O job to ThreadPoolExecutor.
      • case ["SUBMIT", "CPU", task_id, limit_num_str] → Submit CPU job to ProcessPoolExecutor.
      • case ["STREAM", "RESULTS"] → Process completed futures as they arrive via as_completed().
      • case ["EXPORT", "CSV"] → Export completed task audit history to CSV.
      • case ["EXIT" | "QUIT"] → Terminate session safely using a sentinel flag.
      • case _ → Output command error message.
  13. Conditionals, Formatting & Foundations (Lessons 1-11):
    • Sanitize all inputs using .strip() and .upper().
    • Format tabular reports using f-strings with field width alignment specifiers (:<15, :>10) and clear visual borders.

10. File & Workspace Directory Structure

Standard Course Workspace Layout:

Ensure all exercise files are stored inside their corresponding lesson directories:

python_mastery_course/
│
├── lesson_01/ ... lesson_44/
│
└── lesson_45/
    ├── threading_lock_demo.py
    ├── multiprocessing_basic_demo.py
    ├── executor_pools_demo.py
    ├── match_case_concurrency_dispatcher.py
    ├── concurrent_downloader_task.py   <-- (Task 1)
    ├── parallel_math_task.py           <-- (Task 2)
    └── concurrency_engine_os.py        <-- (Task 3: Master Review Capstone)
        

11. What We Will Learn Next

Next Up: Lesson 46 — Asyncio and async-await

Now that you master Threads, Multiprocessing, and Thread Locks, we will explore high-performance single-threaded Asynchronous I/O!

In the next lesson, we will cover:

  • Understanding Asynchronous Non-Blocking I/O vs Multithreading.
  • The Event Loop architecture in Python's standard `asyncio` module.
  • Writing Coroutines using the `async` and `await` keywords.
  • Managing concurrent coroutines using asyncio.gather() and asyncio.TaskGroup().
  • Asynchronous context managers (`async with`) and asynchronous iterators (`async for`).
  • Combining Async Task streams with match-case pattern dispatchers.

📝 Live Lesson Practice

HTML/CSS JavaScript Python C++ C PHP
⌨️ Practice Inputs (लाइव इनपुट भरें) (खाली होने पर RED, भरने पर GREEN underline)
💻 Code Editor (Monaco VS Code Engine)
👀 Live Preview
Address Contact

+91 7877547686

E-mail

onlinecbtportal@gmail.com

Helpline Number

+91 7877547686


Click To Download
Get it on Google Play

ऐप डाउनलोड करने
के लिए Google Play पर
उपलब्ध है

WhatsApp Chat