1. Thread Introduction
Threads are the execution units within a process (a process allocates memory/resources, while threads execute code). Unlike processes, threads do not require new memory space or code duplication, making them lighter and faster to create.
Why Use Threads?
- Threads are cheaper than processes (no memory allocation or code copying).
- Enable concurrent execution of tasks within a single process.
Thread Types
- User-Level Threads: Managed by user-space code (no kernel involvement). Efficient but cannot utilize multi-core CPUs.
- Kernel-Level Threads: Managed by the OS kernel. Can utilize multi-core CPUs (e.g., Windows threads) but involve kernel-mode switches.
2. Implementing Threads in Python
Use the threading module (similar to multiprocessing for processes).
The threading Module
threading.Thread Class
Create threads via two methods: target function or subclassing Thread.
1. Creating Threads
Method 1: Target Function
from threading import Thread
def worker(name):
print(f"{name} is running")
# Create and start thread
t = Thread(target=worker, args=('Alice',))
t.start()
print("Main thread")
Output: Alice is running → Main thread (threads start quickly).
Method 2: Subclassing Thread
from threading import Thread
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.worker_name = name
def run(self):
print(f"{self.worker_name} is running")
# Create and start thread
t = MyThread('Bob')
t.start()
Output: Bob is running
TCP Server with Threads
Handle multiple clients concurrently using threads:
Server Code
from threading import Thread
import socket
# Create socket
s = socket.socket()
s.bind(('127.0.0.1', 9999))
s.listen(5)
def handle_client(client_sock):
while True:
data = client_sock.recv(1024)
print(data.decode('utf8'))
client_sock.send('Server response'.encode('utf8'))
# Accept clients and start threads
while True:
client, addr = s.accept()
t = Thread(target=handle_client, args=(client,))
t.start()
Client Code
import socket
client = socket.socket()
client.connect(('127.0.0.1', 9999))
while True:
msg = input('>>> ').strip()
client.send(msg.encode())
response = client.recv(1024)
print(response.decode())
Thread Join Method
The join() method makes the main thread wait for a child thread to finish:
from threading import Thread
import time
def worker(name):
time.sleep(2)
print(f"{name} is running")
t = Thread(target=worker, args=('Dave',))
t.start()
t.join() # Main thread waits here
print("Main thread continues")
Output: Dave is running → Main thread continues
Shared Data Between Threads
Threads in the same process share memory:
from threading import Thread
username = 'Eve'
def worker():
global username
username = 'Eve_modified'
print(f"{username} (thread)")
t = Thread(target=worker)
t.start()
t.join()
print(f"{username} (main thread)")
Output: Eve_modified (thread) → Eve_modified (main thread)
Thread Object Properties/Methods
1. Same Process Check
Threads share the same process (verify via os.getpid()):
from threading import Thread
import os
def worker():
print(f"Child thread PID: {os.getpid()}")
t = Thread(target=worker)
t.start()
t.join()
print(f"Main thread PID: {os.getpid()}")
2. Active Thread Count
Use threading.active_count() (includes the main thread):
from threading import Thread, active_count
import time
def worker(name):
time.sleep(1)
print(f"{name} is running")
t = Thread(target=worker, args=('Frank',))
t.start()
print(active_count()) # Output: 2 (main + child)
print("Main thread")
3. Thread Name
Get the thread name via current_thread().name or self.name (subclass):
from threading import Thread, current_thread
import time
def worker():
time.sleep(1)
print(current_thread().name)
t = Thread(target=worker)
t.start()
print("Main thread", current_thread().name)
4. Is Thread Alive?
Check if a thread is running with is_alive():
from threading import Thread
import time
def worker(name):
print(f"{name} is running")
time.sleep(2)
print(f"{name} is over")
t = Thread(target=worker, args=('Grace',))
t.start()
print(t.is_alive()) # Output: True
print("Main thread")
GIL (Global Interpreter Lock)
In CPython, the GIL is a mutex that allows only one thread to execute Python bytecode at a time. This limits multi-core utilization (due to non-thread-safe memory management). For CPU-bound tasks, use multiprocessing; for I/O-bound tasks, threads (via multitasking) are efficieent.
Deadlock
A deadlock occurs when threads wait for each other’s locks indefinitely:
from threading import Thread, Lock
import time
lock_a = Lock()
lock_b = Lock()
class DeadlockThread(Thread):
def run(self):
self.acquire_locks()
self.release_locks()
def acquire_locks(self):
lock_a.acquire()
print(f"{self.name} acquired Lock A")
lock_b.acquire()
print(f"{self.name} acquired Lock B")
def release_locks(self):
lock_b.release()
lock_a.release()
for _ in range(2):
t = DeadlockThread()
t.start()
Semaphore
A semaphore manages multiple locks (e.g., limit concurrent threads):
from threading import Thread, Semaphore
import time
import random
sem = Semaphore(3) # Allow 3 concurrent threads
def task(name):
with sem: # Acquire/release automatically
print(f"{name} is working")
time.sleep(random.randint(1, 2))
print(f"{name} finished")
for i in range(10):
t = Thread(target=task, args=(f"Task-{i}",))
t.start()
Event
An Event synchronizes threads (e.g., a traffic light):
from threading import Thread, Event
import time
traffic_light = Event()
def light():
print("Red light!")
time.sleep(2)
print("Green light!")
traffic_light.set() # Wake waiting threads
def car(name):
print(f"{name} waiting for green...")
traffic_light.wait() # Block until event is set
print(f"{name} drives!")
# Start traffic light thread
t_light = Thread(target=light)
t_light.start()
# Start car threads
for i in range(5):
t_car = Thread(target=car, args=(f"Car-{i}",))
t_car.start()
Thread/Process Pools
Pools limit concurrent threads/processes (reuse resources):
Thread Pool
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
import time
# Create a thread pool with 3 threads
pool = ThreadPoolExecutor(max_workers=3)
def task(x):
time.sleep(1)
print(f"Task {x} (Thread: {current_thread().name})")
return x * 2
# Submit tasks (asynchronous)
for i in range(5):
future = pool.submit(task, i)
print(future.result()) # Wait for result (synchronous)
Process Pool
from concurrent.futures import ProcessPoolExecutor
import os
import time
# Create a process pool with 2 processes
pool = ProcessPoolExecutor(max_workers=2)
def process_task(x):
time.sleep(1)
print(f"Task {x} (PID: {os.getpid()})")
return x * 2
if __name__ == "__main__":
for i in range(5):
future = pool.submit(process_task, i)
print(future.result()) # Wait for result (synchronous)