Day 20: Multithreading & Multiprocessing — Running in Parallel

🐍 Day 20: Multithreading & Multiprocessing — Running in Parallel


1. Learning Objectives

By the end of Day 20, you will be able to:

  • Distinguish between concurrency and parallelism
  • Understand the Global Interpreter Lock (GIL) and how it affects threading
  • Use the threading module to run I/O‑bound tasks concurrently
  • Use the multiprocessing module to run CPU‑bound tasks in parallel
  • Know when to choose threading vs. multiprocessing vs. asyncio
  • Apply thread‑safe communication using queue.Queue
  • Avoid common pitfalls like race conditions and deadlocks

2. Concept Explanation

2.1 Concurrency vs. Parallelism — The Dinner Party Analogy

  • Concurrency: One person cooking multiple dishes, rapidly switching between tasks. (One chef, multiple meals in progress.)
  • Parallelism: Several cooks working on different dishes simultaneously. (Multiple chefs, multiple meals finished faster.)

Python's threading provides concurrency (interleaved execution), while multiprocessing provides true parallelism (multiple cores).


2.2 The Global Interpreter Lock (GIL) — The Bottleneck

Python's reference implementation (CPython) has a GIL: a mutex that allows only one thread to execute Python bytecode at a time, even on multi‑core CPUs.

  • I/O‑bound tasks (network, file I/O, user input) release the GIL, so threading helps.
  • CPU‑bound tasks (calculations, image processing) hold the GIL; threading doesn't improve performance. Use multiprocessing here.

2.3 The threading Module — Running Multiple Things "At Once"

import threading
import time

def worker(name, delay):
    for i in range(3):
        print(f"{name}: iteration {i}")
        time.sleep(delay)

# Create threads
t1 = threading.Thread(target=worker, args=("Thread-A", 0.5))
t2 = threading.Thread(target=worker, args=("Thread-B", 0.2))

# Start threads
t1.start()
t2.start()

# Wait for both to finish
t1.join()
t2.join()
print("All threads finished.")

Key points:

  • Threads run concurrently; output may interleave.
  • join() blocks until the thread completes.
  • Use daemon threads for background tasks (they exit when main exits).

2.4 The multiprocessing Module — Real Parallelism

Each process has its own Python interpreter and memory space, bypassing the GIL.

import multiprocessing

def square(number):
    return number * number

if __name__ == "__main__":
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(square, range(10))
    print(results)

Important: The if __name__ == "__main__" guard is mandatory on Windows to avoid infinite process spawning.


2.5 Communication Between Threads / Processes

Threads share memory, so use locks or a thread‑safe queue:

from queue import Queue

q = Queue()
q.put("data")
item = q.get()

Processes don't share memory; use multiprocessing.Queue or Pipe:

from multiprocessing import Queue

q = Queue()
q.put([1, 2, 3])
print(q.get())

2.6 When to Use What

ScenarioTool
Network requests, file I/O, GUI responsivenessthreading
CPU‑heavy calculations, data crunchingmultiprocessing
High‑level async I/O (many connections)asyncio (Day 23 preview)
Simple parallel loopsconcurrent.futures (ThreadPoolExecutor / ProcessPoolExecutor)

2.7 Common Pitfalls

  • Race conditions: Two threads updating a shared variable without synchronisation → corrupted data. Use threading.Lock.
  • Deadlock: Two threads each waiting for a lock the other holds.
  • Overhead: Spawning too many threads or processes can hurt performance.
  • GIL forgetfulness: Using threading for CPU‑bound tasks gives no speedup.

3. Code Examples

Example 1: Downloading URLs with Threads (I/O‑bound)

import threading
import requests
import time

def download(url, thread_name):
    print(f"{thread_name} starting download: {url}")
    response = requests.get(url)
    print(f"{thread_name} finished, status {response.status_code}")

urls = [
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/2",
    "https://httpbin.org/delay/1",
]

start = time.time()
threads = []
for i, url in enumerate(urls):
    t = threading.Thread(target=download, args=(url, f"T{i}"))
    threads.append(t)
    t.start()

for t in threads:
    t.join()
print(f"Total time: {time.time() - start:.1f}s")

Example 2: CPU‑Heavy with Multiprocessing

import multiprocessing
import math

def compute_pi(dummy):
    """A dummy function that runs a heavy computation."""
    return sum(1.0/(k*k) for k in range(1, 3000000))

if __name__ == "__main__":
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(compute_pi, range(4))
    print(f"Results: {results}")

Example 3: concurrent.futures — Simpler Interface

from concurrent.futures import ThreadPoolExecutor, as_completed

def fetch(url):
    import requests
    return requests.get(url).status_code

urls = ["https://httpbin.org/get"] * 4

with ThreadPoolExecutor(max_workers=4) as executor:
    future_to_url = {executor.submit(fetch, url): url for url in urls}
    for future in as_completed(future_to_url):
        url = future_to_url[future]
        print(f"{url} -> {future.result()}")

4. Hands-On Exercises

Exercise 1: Threaded Counter

Create a thread‑safe counter using threading.Lock. Two threads each increment the counter 1000 times. Print the final count.

Exercise 2: Concurrent File Reader

Write a program that reads two large text files simultaneously using threads and counts lines. Print the total line count.

Exercise 3: Prime Checker with Multiprocessing

Given a list of numbers, use a multiprocessing.Pool to determine which ones are prime. Print the primes.

Exercise 4: Producer‑Consumer with Queue

Implement a producer thread that generates numbers and a consumer thread that squares them, using a queue.Queue. The producer sends 10 numbers, the consumer prints the squares.

Exercise 5: Compare Sequential vs. Threaded vs. Multiprocessing

Measure the time to calculate the sum of squares of numbers 1 to 10,000,000 three ways: sequentially, with 2 threads, and with 2 processes. (Note: threading won't help here; observe the difference.)


5. Applied Challenge Task 🏗️

Web Scraper with Thread Pool

Build a concurrent web scraper that fetches titles from a list of URLs and saves them to a file.

Requirements:

  1. Accept a list of at least 10 URLs (use httpbin.org or a test site).
  2. Use concurrent.futures.ThreadPoolExecutor to fetch all URLs concurrently (max 5 workers).
  3. Extract the <title> from each response (use regex or just return the full text if no title).
  4. Handle timeouts and connection errors gracefully, skipping failed URLs.
  5. Write the results (URL + title) to a JSON file titles.json.
  6. Measure the total time and compare with sequential execution.

Stretch goals:

  • Add retry logic (exponential backoff) for failed URLs.
  • Implement a progress bar using tqdm.
  • Fetch meta descriptions as well.

6. Brief Review Summary

ConceptKey Points
Concurrency vs. ParallelismConcurrency = interleaving; Parallelism = simultaneous
GILLimits Python bytecode execution to one thread at a time
ThreadingGood for I/O‑bound tasks; light‑weight, shared memory
MultiprocessingGood for CPU‑bound tasks; separate memory, true parallelism
queue.QueueThread‑safe communication
concurrent.futuresHigh‑level interface for thread/process pools
PitfallsRace conditions, deadlocks, GIL ignorance

7. Preview of Next Topic — Day 21

Tomorrow we'll add persistence to our programs:

  • Introduction to Databases — why they matter
  • SQLite — a light, built‑in database engine
  • Creating tables, inserting, querying, updating, and deleting data
  • Using sqlite3 from Python
  • Best practices for database connections

🎯 Your Action Items for Day 20:

  1. ✅ Complete all 5 exercises
  2. ✅ Build the Web Scraper with Thread Pool challenge
  3. ✅ Observe the GIL effect: compare threaded vs sequential for a CPU‑bound task
  4. ✅ Experiment with ThreadPoolExecutor vs ProcessPoolExecutor

Comments

Popular posts from this blog

Day 1: Welcome to Python — Your Journey Begins

Python: Your Gateway to Coding Adventures

Day 11: List Comprehensions & Lambda Functions

Earn From the Comfort of Your Home

Build the skills to work comfortably from home, on your own terms.

Get Started Today