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 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()
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
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")
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)
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 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.
multiprocessing module which creates separate processes, each with its own GIL.
# 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()
ThreadPoolExecutor for I/O-bound tasks and ProcessPoolExecutor for CPU-bound tasks to keep your code clean and maintainable.
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.