← Back to Tutorials Chapter 13

Multithreading

Python threading model

The Thread Class

The threading module provides a high-level interface for threading. A thread is a separate flow of execution within a process.

import threading
import time

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

t1 = threading.Thread(target=worker, args=("Thread-A", 0.5))
t2 = threading.Thread(target=worker, args=("Thread-B", 0.3))

t1.start()
t2.start()

t1.join()
t2.join()
print("Both threads completed.")

Daemon Threads

Daemon threads run in the background and automatically exit when the main program ends. They are useful for monitoring or housekeeping tasks.

daemon = threading.Thread(target=monitor, daemon=True)
daemon.start()

Thread Synchronization with Lock

Locks prevent race conditions by ensuring only one thread accesses a shared resource at a time.

lock = threading.Lock()
counter = 0

def increment():
    global counter
    for _ in range(100000):
        lock.acquire()
        counter += 1
        lock.release()

# Using context manager (preferred)
def decrement():
    global counter
    for _ in range(100000):
        with lock:
            counter -= 1

RLock (Reentrant Lock)

An RLock can be acquired multiple times by the same thread without blocking. Useful when a function calls another function that needs the same lock.

rlock = threading.RLock()

def outer():
    with rlock:
        inner()

def inner():
    with rlock:
        print("Inside inner function")

Semaphore

A Semaphore limits access to a resource to a fixed number of threads at a time.

pool = threading.Semaphore(3)

def limited_task(n):
    with pool:
        print(f"Task {n} running")
        time.sleep(1)

ThreadPoolExecutor (concurrent.futures)

The ThreadPoolExecutor provides a simpler, higher-level interface for managing a pool of threads.

from concurrent.futures import ThreadPoolExecutor

def fetch_url(url):
    # Simulate fetching a URL
    time.sleep(1)
    return f"Data from {url}"

urls = ["http://example.com", "http://test.org", "http://demo.net"]

with ThreadPoolExecutor(max_workers=3) as executor:
    results = executor.map(fetch_url, urls)

for result in results:
    print(result)

The Global Interpreter Lock (GIL)

The GIL is a mutex in CPython that prevents multiple threads from executing Python bytecode simultaneously. This means CPU-bound tasks do not benefit from threading — use multiprocessing instead. Threading is still useful for I/O-bound tasks.

Note: For CPU-bound operations, use the multiprocessing module which creates separate processes, each with its own GIL.

Threading vs Multiprocessing

# Threading - good for I/O-bound tasks
import threading
threading.Thread(target=io_task).start()

# Multiprocessing - good for CPU-bound tasks
import multiprocessing
multiprocessing.Process(target=cpu_task).start()
Best Practice: Use ThreadPoolExecutor for I/O-bound tasks and ProcessPoolExecutor for CPU-bound tasks to keep your code clean and maintainable.
Exercise: Write a program that downloads 10 URLs concurrently using ThreadPoolExecutor. Measure the total time and compare it with sequential execution. Then implement a thread-safe counter using a Lock and verify it works correctly with 100 threads incrementing it 1000 times each.