Advertisement
🚀 Advanced Python

Python Multithreading – Concurrent Execution with Threads

Multithreading lets your program do multiple things at the same time. Python threads are ideal for I/O-bound tasks: network requests, file operations, database queries. Understanding the GIL (Global Interpreter Lock) helps you choose between threads and processes for different workloads.

⏱️ 25 min read🎯 Advanced📅 Updated 2026

Creating Threads

Use threading.Thread to create and start threads.

Python
import threading
import time

def download(site):
    print(f"Downloading {site}...")
    time.sleep(2)   # Simulate network I/O
    print(f"Done: {site}")

# Without threads: takes 6 seconds
# With threads: takes ~2 seconds
threads = []
for site in ["google.com", "github.com", "python.org"]:
    t = threading.Thread(target=download, args=(site,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()   # Wait for all to complete
print("All downloads done")
▶ Output
Downloading google.com... Downloading github.com... Downloading python.org... Done: google.com Done: github.com Done: python.org All downloads done

The GIL – Global Interpreter Lock

The GIL prevents multiple threads from executing Python bytecode simultaneously. This means threads DO NOT speed up CPU-bound tasks (computation). They DO help I/O-bound tasks because threads release the GIL while waiting for I/O.

⚠️
Note

Use threading for I/O-bound tasks (HTTP, files, databases). Use multiprocessing for CPU-bound tasks (calculations, image processing). Using threads for heavy computation will NOT improve performance due to the GIL.

Thread Safety – Using Locks

When threads share data, use a Lock to prevent race conditions.

Python
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100_000):
        with lock:      # Only one thread at a time
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(5)]
for t in threads: t.start()
for t in threads: t.join()

print(f"Counter: {counter}")  # Always 500000 with lock
▶ Output
Counter: 500000
Advertisement

ThreadPoolExecutor – High-Level API

concurrent.futures.ThreadPoolExecutor is the modern, higher-level threading API.

Python
from concurrent.futures import ThreadPoolExecutor
import time

def fetch(url):
    time.sleep(1)   # Simulate I/O
    return f"Data from {url}"

urls = ["api.com/1", "api.com/2", "api.com/3"]

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

for r in results:
    print(r)
▶ Output
Data from api.com/1 Data from api.com/2 Data from api.com/3

The GIL: Why Python Threads Don't Speed Up Math

CPython has a Global Interpreter Lock — only one thread executes Python bytecode at a time. So threads give you concurrency (overlapping waits) but not parallelism (using multiple cores) for pure-Python computation.

TaskThreads help?Why
Download 100 URLs✅ yesthreads wait on I/O — GIL released during the wait
Sum a huge list of numbers❌ noCPU-bound — GIL serializes it anyway
from concurrent.futures import ThreadPoolExecutor
# I/O-bound → threads shine
with ThreadPoolExecutor() as pool:
    pages = list(pool.map(download, urls))

from concurrent.futures import ProcessPoolExecutor
# CPU-bound → separate processes, each its OWN GIL/core
with ProcessPoolExecutor() as pool:
    results = list(pool.map(heavy_math, chunks))

The rule of thumb: I/O-bound → threading or asyncio; CPU-bound → multiprocessing (dodges the GIL by running real separate processes). Also watch shared state: because threads interleave, unguarded shared variables cause race conditions — protect them with a threading.Lock.

🏋️ Practical Exercise

Work with threads:

  1. Create and start two Thread objects running the same function, then join() them.
  2. Show that a shared counter without a lock can produce wrong results.
  3. Fix it using a threading.Lock.
  4. Use ThreadPoolExecutor to download (simulate) several URLs concurrently.

🔥 Challenge Exercise

Simulate downloading 10 web pages where each “download” sleeps for a second. Run them serially and time it, then run them with a ThreadPoolExecutor and show the dramatic speedup for this I/O-bound work. Add a shared results dictionary protected by a Lock to demonstrate thread-safe updates. Bonus: explain why the same approach would NOT speed up a CPU-bound version (the GIL).

📋 Summary

  • Threads run within a single process and share memory.
  • The Global Interpreter Lock (GIL) lets only one thread execute Python bytecode at a time.
  • Because of the GIL, threading helps I/O-bound tasks but not CPU-bound ones.
  • Shared mutable state can cause race conditions; protect it with a threading.Lock.
  • ThreadPoolExecutor from concurrent.futures offers a clean high-level API.
  • For CPU-bound parallelism, use multiprocessing instead.

Interview Questions on Multithreading

  • What is a thread and what is multithreading?
  • What is the GIL and how does it affect Python threads?
  • When is threading beneficial in Python despite the GIL?
  • What is a race condition and how do locks prevent it?
  • What is the difference between Lock and RLock?
  • What does ThreadPoolExecutor provide?
  • What is the difference between threading and multiprocessing?

FAQ

What is the GIL? +

The Global Interpreter Lock is a mutex in CPython that allows only one thread to run Python bytecode at a time. It simplifies memory management but prevents threads from running CPU-bound Python code in true parallel.

If the GIL exists, is threading useless? +

No. The GIL is released during blocking I/O (network, disk), so threads still overlap waiting and speed up I/O-bound work significantly. It only limits CPU-bound parallelism, where multiprocessing is the answer.

What is a race condition? +

It is a bug where the result depends on the unpredictable timing of threads accessing shared data — for example, two threads incrementing the same counter and losing updates. A Lock serializes access to prevent it.

What is the difference between threading and multiprocessing? +

Threads share memory within one process and are limited by the GIL; great for I/O. Processes have separate memory and their own GIL, giving true CPU parallelism at the cost of higher overhead.