Thread Basics¶
The threading module provides a way to run multiple threads (lightweight processes) within a single Python process.
Mental Model
A thread is a separate line of execution inside the same program, sharing all memory with the main thread. Creating one is like hiring an assistant who works at the same desk -- fast to start and can see all your files, but you need coordination to avoid stepping on each other. Use start() to launch, join() to wait for completion.
Creating Threads¶
Method 1: Using Thread with target Function¶
```python import threading import time
def worker(name, delay): """Function to run in a thread.""" print(f"{name}: Starting") time.sleep(delay) print(f"{name}: Finished")
Create thread¶
thread = threading.Thread(target=worker, args=("Thread-1", 2))
Start thread¶
thread.start()
print("Main: Thread started")
Wait for thread to complete¶
thread.join()
print("Main: Thread finished") ```
Output:
Thread-1: Starting
Main: Thread started
Thread-1: Finished
Main: Thread finished
Method 2: Subclassing Thread¶
```python import threading import time
class WorkerThread(threading.Thread): def init(self, name, delay): super().init() self.name = name self.delay = delay self.result = None
def run(self):
"""Override run() method."""
print(f"{self.name}: Starting")
time.sleep(self.delay)
self.result = f"{self.name} completed"
print(f"{self.name}: Finished")
Create and start¶
thread = WorkerThread("Worker-1", 2) thread.start() thread.join()
print(f"Result: {thread.result}") ```
Thread Lifecycle¶
┌─────────┐
│ Created │
└────┬────┘
│ start()
▼
┌─────────┐
┌──────────│ Running │──────────┐
│ └────┬────┘ │
│ │ │
wait/sleep complete exception
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ Blocked │ │ Finished │ │ Finished│
└────┬────┘ └──────────┘ └─────────┘
│
resume
│
└──────────► Running
Thread States¶
```python import threading import time
def worker(): time.sleep(1)
thread = threading.Thread(target=worker)
print(f"Created - is_alive: {thread.is_alive()}") # False
thread.start() print(f"Started - is_alive: {thread.is_alive()}") # True
thread.join() print(f"Joined - is_alive: {thread.is_alive()}") # False ```
Starting and Joining Threads¶
start() — Begin Execution¶
```python import threading
def task(): print("Task running")
thread = threading.Thread(target=task) thread.start() # Returns immediately, thread runs in background
Can only start a thread once¶
thread.start() # RuntimeError: threads can only be started once¶
```
join() — Wait for Completion¶
```python import threading import time
def slow_task(): time.sleep(2) print("Slow task done")
thread = threading.Thread(target=slow_task) thread.start()
print("Waiting for thread...") thread.join() # Blocks until thread completes print("Thread finished")
With timeout¶
thread2 = threading.Thread(target=slow_task) thread2.start() thread2.join(timeout=1) # Wait at most 1 second
if thread2.is_alive(): print("Thread still running after timeout") ```
Multiple Threads¶
Creating Multiple Threads¶
```python import threading import time
def worker(worker_id): print(f"Worker {worker_id}: Starting") time.sleep(1) print(f"Worker {worker_id}: Done")
Create threads¶
threads = [] for i in range(5): t = threading.Thread(target=worker, args=(i,)) threads.append(t)
Start all threads¶
for t in threads: t.start()
Wait for all threads to complete¶
for t in threads: t.join()
print("All workers finished") ```
Compact Pattern¶
```python import threading
def worker(n): return n * 2
Create, start, and collect threads¶
threads = [] for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() threads.append(t)
Wait for all¶
for t in threads: t.join() ```
Thread Arguments¶
Positional Arguments (args)¶
```python import threading
def greet(name, greeting): print(f"{greeting}, {name}!")
Pass as tuple¶
thread = threading.Thread(target=greet, args=("Alice", "Hello")) thread.start() thread.join() ```
Keyword Arguments (kwargs)¶
```python import threading
def greet(name, greeting="Hi"): print(f"{greeting}, {name}!")
Pass as dict¶
thread = threading.Thread(target=greet, kwargs={"name": "Bob", "greeting": "Hey"}) thread.start() thread.join()
Mixed¶
thread = threading.Thread(target=greet, args=("Charlie",), kwargs={"greeting": "Howdy"}) thread.start() thread.join() ```
Getting Results from Threads¶
Method 1: Shared Variable¶
```python import threading
results = {} lock = threading.Lock()
def compute(task_id, value): result = value ** 2 with lock: results[task_id] = result
threads = [] for i in range(5): t = threading.Thread(target=compute, args=(i, i + 10)) t.start() threads.append(t)
for t in threads: t.join()
print(results) # {0: 100, 1: 121, 2: 144, 3: 169, 4: 196} ```
Method 2: Thread-Safe Queue¶
```python import threading import queue
def compute(value, result_queue): result = value ** 2 result_queue.put((value, result))
result_queue = queue.Queue() threads = []
for i in range(5): t = threading.Thread(target=compute, args=(i, result_queue)) t.start() threads.append(t)
for t in threads: t.join()
Collect results¶
results = [] while not result_queue.empty(): results.append(result_queue.get())
print(results) ```
Method 3: Thread Subclass with Attribute¶
```python import threading
class ComputeThread(threading.Thread): def init(self, value): super().init() self.value = value self.result = None
def run(self):
self.result = self.value ** 2
threads = [ComputeThread(i) for i in range(5)]
for t in threads: t.start()
for t in threads: t.join()
results = [t.result for t in threads] print(results) # [0, 1, 4, 9, 16] ```
Thread Properties¶
Thread Name¶
```python import threading
def worker(): print(f"Running in: {threading.current_thread().name}")
Auto-generated name¶
t1 = threading.Thread(target=worker) t1.start() # "Thread-1"
Custom name¶
t2 = threading.Thread(target=worker, name="MyWorker") t2.start() # "MyWorker" ```
Daemon Threads¶
Daemon threads are automatically killed when the main program exits:
```python import threading import time
def background_task(): while True: print("Background running...") time.sleep(1)
Non-daemon (default): program waits for thread¶
t1 = threading.Thread(target=background_task) t1.daemon = False # Default
t1.start() # Program would never exit!¶
Daemon: thread killed when main exits¶
t2 = threading.Thread(target=background_task, daemon=True) t2.start()
time.sleep(3) print("Main exiting...")
Daemon thread is killed here¶
```
Current Thread Info¶
```python import threading
def show_info(): current = threading.current_thread() print(f"Name: {current.name}") print(f"Ident: {current.ident}") print(f"Native ID: {current.native_id}") print(f"Daemon: {current.daemon}") print(f"Is alive: {current.is_alive()}")
thread = threading.Thread(target=show_info, name="InfoThread") thread.start() thread.join()
Main thread info¶
print(f"\nMain thread: {threading.main_thread().name}") print(f"Active threads: {threading.active_count()}") print(f"All threads: {threading.enumerate()}") ```
Exception Handling¶
Exceptions in Threads¶
Exceptions in threads don't propagate to the main thread:
```python import threading import time
def risky_task(): time.sleep(0.5) raise ValueError("Something went wrong!")
thread = threading.Thread(target=risky_task) thread.start() thread.join()
print("Main continues...") # This still runs!
Exception is printed but not raised in main thread¶
```
Catching Exceptions¶
```python import threading import traceback
class SafeThread(threading.Thread): def init(self, args, kwargs): super().init(args, **kwargs) self.exception = None
def run(self):
try:
if self._target:
self._target(*self._args, **self._kwargs)
except Exception as e:
self.exception = e
self.traceback = traceback.format_exc()
def risky_task(): raise ValueError("Error!")
thread = SafeThread(target=risky_task) thread.start() thread.join()
if thread.exception: print(f"Thread raised: {thread.exception}") print(thread.traceback) ```
Practical Example: Parallel Downloads¶
```python import threading import time import random
def download_file(filename): """Simulate file download.""" print(f"Downloading {filename}...") # Simulate varying download times time.sleep(random.uniform(0.5, 2.0)) print(f"Completed {filename}") return f"{filename}: {random.randint(100, 1000)} bytes"
def download_sequential(files): """Download files one by one.""" results = [] for f in files: results.append(download_file(f)) return results
def download_parallel(files): """Download files in parallel using threads.""" results = [] lock = threading.Lock()
def download_and_store(filename):
result = download_file(filename)
with lock:
results.append(result)
threads = []
for f in files:
t = threading.Thread(target=download_and_store, args=(f,))
t.start()
threads.append(t)
for t in threads:
t.join()
return results
Test¶
files = ["file1.zip", "file2.zip", "file3.zip", "file4.zip", "file5.zip"]
Sequential¶
start = time.perf_counter() download_sequential(files) seq_time = time.perf_counter() - start print(f"\nSequential: {seq_time:.2f}s")
Parallel¶
start = time.perf_counter() download_parallel(files) par_time = time.perf_counter() - start print(f"Parallel: {par_time:.2f}s") print(f"Speedup: {seq_time/par_time:.1f}x") ```
Key Takeaways¶
- Create threads with
threading.Thread(target=func, args=()) - Call
start()to begin execution,join()to wait for completion - Threads share memory — use locks for safe access
- Get results via shared variables, queues, or thread subclass attributes
- Daemon threads are killed when main program exits
- Exceptions in threads don't propagate — handle them explicitly
- Best for I/O-bound tasks where GIL is released
Runnable Example: threading_basics_tutorial.py¶
```python """ Topic 45.2 - Threading Basics with threading.Thread
Complete guide to Python's threading module, covering thread creation, management, and basic patterns.
Learning Objectives: - Create and start threads - Pass arguments to threads - Wait for thread completion (join) - Daemon threads - Thread naming and identification - Thread-local storage
Author: Python Educator Date: 2024 """
import threading import time import random from queue import Queue
============================================================================¶
PART 1: BEGINNER - Creating and Starting Threads¶
============================================================================¶
def basic_thread_creation(): """ The most fundamental way to create a thread: using threading.Thread with a target function. """ print("=" * 70) print("BEGINNER: Creating Your First Thread") print("=" * 70)
def worker():
"""Simple function that will run in a separate thread"""
print(f" Worker thread started: {threading.current_thread().name}")
time.sleep(1) # Simulate some work
print(f" Worker thread finished: {threading.current_thread().name}")
print("\n📝 Creating a thread:")
print(" thread = threading.Thread(target=worker)")
print(" thread.start()")
# Create the thread
thread = threading.Thread(target=worker)
print(f"\nMain thread: {threading.current_thread().name}")
print("Starting worker thread...")
# Start the thread (begins execution)
thread.start()
print("Main thread continues while worker runs...")
# Wait for the thread to complete
thread.join()
print("Worker thread has finished. Main thread exiting.\n")
print("=" * 70 + "\n")
def threads_with_arguments(): """ Pass arguments to thread functions using args and kwargs. """ print("=" * 70) print("BEGINNER: Passing Arguments to Threads") print("=" * 70)
def greet(name, greeting="Hello"):
"""
Function that takes arguments - will be run in a thread.
Args:
name: Person's name
greeting: Greeting message (default: "Hello")
"""
thread_name = threading.current_thread().name
print(f"[{thread_name}] {greeting}, {name}!")
time.sleep(0.5)
print("\n📝 Method 1: Using args tuple")
# Pass arguments as tuple
thread1 = threading.Thread(target=greet, args=("Alice",))
thread1.start()
thread1.join()
print("\n📝 Method 2: Using kwargs dictionary")
# Pass arguments as keyword arguments
thread2 = threading.Thread(
target=greet,
kwargs={"name": "Bob", "greeting": "Hi"}
)
thread2.start()
thread2.join()
print("\n📝 Method 3: Both args and kwargs")
# Mix positional and keyword arguments
thread3 = threading.Thread(
target=greet,
args=("Charlie",),
kwargs={"greeting": "Hey"}
)
thread3.start()
thread3.join()
print("\n" + "=" * 70 + "\n")
def multiple_threads_example(): """ Create and manage multiple threads simultaneously. """ print("=" * 70) print("BEGINNER: Running Multiple Threads") print("=" * 70)
def download_file(file_id, duration):
"""
Simulate downloading a file.
Args:
file_id: File identifier
duration: Download duration in seconds
"""
thread = threading.current_thread().name
print(f"[{thread}] Starting download of file {file_id}")
time.sleep(duration) # Simulate download time
print(f"[{thread}] Completed download of file {file_id}")
print("\n⏱️ Downloading 5 files concurrently...\n")
start_time = time.time()
# Create multiple threads
threads = []
for i in range(5):
# Each download takes 1-2 seconds
duration = random.uniform(1.0, 2.0)
thread = threading.Thread(
target=download_file,
args=(i, duration),
name=f"Downloader-{i}" # Give thread a meaningful name
)
threads.append(thread)
thread.start() # Start immediately
# Wait for all threads to complete
print("Main thread waiting for all downloads to complete...")
for thread in threads:
thread.join() # Block until this thread finishes
elapsed = time.time() - start_time
print(f"\n✓ All downloads completed in {elapsed:.2f} seconds")
print(" (Sequential would have taken ~7.5 seconds)")
print("\n" + "=" * 70 + "\n")
============================================================================¶
PART 2: INTERMEDIATE - Thread Management and Control¶
============================================================================¶
def daemon_threads_explained(): """ Daemon threads are background threads that don't prevent program exit. They're useful for background tasks that should stop when main exits. """ print("=" * 70) print("INTERMEDIATE: Daemon Threads") print("=" * 70)
def background_task(task_id):
"""
Background task that runs indefinitely.
Args:
task_id: Task identifier
"""
try:
while True:
print(f" Background task {task_id} is running...")
time.sleep(1)
except Exception as e:
print(f" Task {task_id} interrupted: {e}")
print("\n📝 Normal (Non-Daemon) Thread:")
print(" Keeps program alive until it completes\n")
# Create a normal thread (daemon=False is default)
normal_thread = threading.Thread(
target=lambda: print(" Normal thread: I'll complete my work"),
daemon=False
)
normal_thread.start()
normal_thread.join() # Wait for it
print(" ✓ Normal thread completed\n")
print("📝 Daemon Thread:")
print(" Automatically stops when main program exits\n")
# Create a daemon thread
daemon_thread = threading.Thread(
target=background_task,
args=(1,),
daemon=True # This makes it a daemon thread
)
print(" Starting daemon thread...")
daemon_thread.start()
# Let it run for a bit
time.sleep(2.5)
print("\n Main thread exiting (daemon will stop automatically)")
print(" Notice: daemon thread doesn't prevent program exit")
print("\n💡 Use Cases for Daemon Threads:")
print(" ✓ Background monitoring")
print(" ✓ Periodic cleanup tasks")
print(" ✓ Logging/metrics collection")
print(" ✓ Keep-alive connections")
print("\n" + "=" * 70 + "\n")
def thread_properties_and_methods(): """ Explore thread properties: name, ident, daemon status, alive status. """ print("=" * 70) print("INTERMEDIATE: Thread Properties and Methods") print("=" * 70)
def worker(duration):
"""Worker that sleeps for specified duration"""
time.sleep(duration)
# Create a thread
thread = threading.Thread(
target=worker,
args=(2,),
name="MyWorkerThread"
)
print("\n📊 Before Starting:")
print(f" Name: {thread.name}")
print(f" Daemon: {thread.daemon}")
print(f" Is alive: {thread.is_alive()}")
print(f" Ident: {thread.ident}") # None until started
# Start the thread
thread.start()
print("\n📊 After Starting:")
print(f" Name: {thread.name}")
print(f" Daemon: {thread.daemon}")
print(f" Is alive: {thread.is_alive()}")
print(f" Ident: {thread.ident}") # Now has an ID
# Wait for completion
thread.join()
print("\n📊 After Completion:")
print(f" Is alive: {thread.is_alive()}")
print(f" Ident: {thread.ident}") # Still has ID
# Current thread info
print("\n📊 Current (Main) Thread:")
current = threading.current_thread()
print(f" Name: {current.name}")
print(f" Ident: {current.ident}")
# All active threads
print("\n📊 All Active Threads:")
for t in threading.enumerate():
print(f" - {t.name} (daemon={t.daemon}, alive={t.is_alive()})")
print("\n" + "=" * 70 + "\n")
def thread_joining_patterns(): """ Different patterns for waiting on threads with join(). """ print("=" * 70) print("INTERMEDIATE: Thread Joining Patterns") print("=" * 70)
def task(task_id, duration):
"""Task that takes specified time to complete"""
print(f" Task {task_id} started")
time.sleep(duration)
print(f" Task {task_id} completed")
# Pattern 1: Join with timeout
print("\n📝 Pattern 1: Join with Timeout")
thread = threading.Thread(target=task, args=(1, 2))
thread.start()
print(" Waiting up to 1 second...")
thread.join(timeout=1.0) # Wait max 1 second
if thread.is_alive():
print(" ⏱️ Timeout! Thread still running")
print(" Continuing without waiting...")
thread.join() # Wait for actual completion
# Pattern 2: Join all threads
print("\n📝 Pattern 2: Join All Threads")
threads = []
for i in range(3):
t = threading.Thread(target=task, args=(i+2, 1))
threads.append(t)
t.start()
print(" Waiting for all threads...")
for t in threads:
t.join()
print(" ✓ All threads completed")
# Pattern 3: Non-blocking check
print("\n📝 Pattern 3: Non-blocking Status Check")
thread = threading.Thread(target=task, args=(5, 1.5))
thread.start()
while thread.is_alive():
print(" Thread still running, doing other work...")
time.sleep(0.5)
print(" ✓ Thread finished")
print("\n" + "=" * 70 + "\n")
============================================================================¶
PART 3: ADVANCED - Thread Classes and Local Storage¶
============================================================================¶
class WorkerThread(threading.Thread): """ Advanced: Custom thread class by inheriting from threading.Thread. Override run() method to define thread behavior. """
def __init__(self, task_name, iterations):
"""
Initialize the custom thread.
Args:
task_name: Name of the task
iterations: Number of iterations to perform
"""
# IMPORTANT: Call parent __init__
super().__init__()
# Store instance variables
self.task_name = task_name
self.iterations = iterations
self.result = None
def run(self):
"""
This method is called when start() is invoked.
Override this to define what the thread does.
"""
print(f"[{self.name}] Starting task: {self.task_name}")
# Perform work
total = 0
for i in range(self.iterations):
total += i
if i % 100000 == 0:
time.sleep(0.01) # Simulate some I/O
# Store result
self.result = total
print(f"[{self.name}] Completed task: {self.task_name}")
print(f"[{self.name}] Result: {self.result}")
def custom_thread_class_example(): """ Demonstrate using a custom thread class. """ print("=" * 70) print("ADVANCED: Custom Thread Class") print("=" * 70)
print("\n📝 Creating custom thread instances:\n")
# Create thread instances
thread1 = WorkerThread("Calculate-A", 500000)
thread2 = WorkerThread("Calculate-B", 300000)
# Give them custom names
thread1.name = "Calculator-1"
thread2.name = "Calculator-2"
# Start them
thread1.start()
thread2.start()
# Wait for completion
thread1.join()
thread2.join()
# Access results
print(f"\n📊 Results:")
print(f" Thread 1 result: {thread1.result}")
print(f" Thread 2 result: {thread2.result}")
print("\n💡 Benefits of Custom Thread Class:")
print(" ✓ Encapsulate thread logic")
print(" ✓ Store thread-specific data")
print(" ✓ Easier to access results")
print(" ✓ More object-oriented design")
print("\n" + "=" * 70 + "\n")
def thread_local_storage_example(): """ Thread-local storage: Each thread gets its own copy of data. Useful for storing per-thread state without passing it around. """ print("=" * 70) print("ADVANCED: Thread-Local Storage") print("=" * 70)
# Create thread-local storage
thread_local = threading.local()
def worker(worker_id):
"""
Each thread stores its own data in thread_local.
Args:
worker_id: Worker identifier
"""
# Store thread-specific data
thread_local.worker_id = worker_id
thread_local.counter = 0
thread_local.name = f"Worker-{worker_id}"
print(f"[{thread_local.name}] Starting work")
# Do some work
for i in range(5):
thread_local.counter += 1
time.sleep(0.1)
print(f"[{thread_local.name}] Counter: {thread_local.counter}")
# Access thread-specific data
print(f"[{thread_local.name}] Final state:")
print(f" Worker ID: {thread_local.worker_id}")
print(f" Counter: {thread_local.counter}")
print("\n📝 Starting threads with thread-local storage:\n")
threads = []
for i in range(3):
thread = threading.Thread(target=worker, args=(i,))
threads.append(thread)
thread.start()
# Wait for all
for thread in threads:
thread.join()
print("\n💡 Key Points:")
print(" • Each thread has its own copy of thread_local data")
print(" • No need for locks when accessing thread_local")
print(" • Data automatically cleaned up when thread exits")
print(" • Useful for database connections, request contexts, etc.")
print("\n" + "=" * 70 + "\n")
def producer_consumer_basic(): """ Advanced pattern: Basic producer-consumer using threads. One thread produces items, another consumes them. """ print("=" * 70) print("ADVANCED: Producer-Consumer Pattern") print("=" * 70)
# Shared queue (thread-safe)
queue = Queue(maxsize=5)
def producer(num_items):
"""
Produce items and put them in the queue.
Args:
num_items: Number of items to produce
"""
for i in range(num_items):
item = f"Item-{i}"
print(f"Producer: Creating {item}")
queue.put(item) # Thread-safe put
time.sleep(0.5) # Simulate production time
# Signal completion
queue.put(None) # Sentinel value
print("Producer: Finished producing")
def consumer():
"""
Consume items from the queue until None is received.
"""
while True:
item = queue.get() # Thread-safe get (blocks if empty)
if item is None:
print("Consumer: Received stop signal")
break
print(f"Consumer: Processing {item}")
time.sleep(0.8) # Simulate processing time
queue.task_done() # Mark as processed
print("Consumer: Finished consuming")
print("\n⚙️ Starting producer-consumer system:\n")
# Create threads
producer_thread = threading.Thread(target=producer, args=(8,))
consumer_thread = threading.Thread(target=consumer)
# Start both
producer_thread.start()
consumer_thread.start()
# Wait for completion
producer_thread.join()
consumer_thread.join()
print("\n✓ Producer-consumer completed")
print("\n💡 This pattern is useful for:")
print(" • Decoupling production and consumption rates")
print(" • Buffering between fast and slow operations")
print(" • Load balancing across multiple workers")
print("\n" + "=" * 70 + "\n")
============================================================================¶
MAIN EXECUTION¶
============================================================================¶
def main(): """Run all threading demonstrations.""" print("\n" + "=" * 70) print(" " * 20 + "THREADING BASICS") print(" " * 15 + "threading.Thread Tutorial") print("=" * 70 + "\n")
# Beginner level
basic_thread_creation()
threads_with_arguments()
multiple_threads_example()
# Intermediate level
daemon_threads_explained()
thread_properties_and_methods()
thread_joining_patterns()
# Advanced level
custom_thread_class_example()
thread_local_storage_example()
producer_consumer_basic()
print("\n" + "=" * 70)
print("Threading Basics Tutorial Complete!")
print("=" * 70)
print("\n💡 Key Takeaways:")
print("1. Use threading.Thread(target=func) to create threads")
print("2. Call start() to begin execution, join() to wait")
print("3. Daemon threads stop automatically when main exits")
print("4. Use thread.name and thread.is_alive() for monitoring")
print("5. Custom thread classes offer better encapsulation")
print("6. Thread-local storage provides per-thread data")
print("=" * 70 + "\n")
if name == "main": main() ```
Exercises¶
Exercise 1.
Create a SquareThread class that subclasses threading.Thread. Its constructor takes a number, and its run method computes and stores the square. Create 5 instances (for numbers 1 through 5), start them all, join them all, and print each thread's result.
Solution to Exercise 1
```python
import threading
class SquareThread(threading.Thread):
def __init__(self, number):
super().__init__()
self.number = number
self.result = None
def run(self):
self.result = self.number ** 2
threads = [SquareThread(i) for i in range(1, 6)]
for t in threads:
t.start()
for t in threads:
t.join()
for t in threads:
print(f"{t.number}^2 = {t.result}")
```
Exercise 2.
Write a program that simulates downloading 8 files concurrently. Each "download" sleeps for a random duration between 0.5 and 1.5 seconds and returns the file name and byte count (a random integer). Use a queue.Queue to collect results from threads. After all threads finish, print the results sorted by file name.
Solution to Exercise 2
```python
import threading
import queue
import time
import random
def download(file_name, result_queue):
duration = random.uniform(0.5, 1.5)
time.sleep(duration)
byte_count = random.randint(1000, 100_000)
result_queue.put((file_name, byte_count))
result_queue = queue.Queue()
files = [f"file_{i}.dat" for i in range(8)]
threads = []
for f in files:
t = threading.Thread(target=download, args=(f, result_queue))
t.start()
threads.append(t)
for t in threads:
t.join()
results = []
while not result_queue.empty():
results.append(result_queue.get())
for name, size in sorted(results):
print(f"{name}: {size} bytes")
```
Exercise 3.
Implement a SafeThread wrapper class that catches exceptions raised inside thread targets and stores them. Launch 5 threads where some succeed and some raise ValueError. After joining, iterate over the threads and print which succeeded and which failed (with the exception message).
Solution to Exercise 3
```python
import threading
class SafeThread(threading.Thread):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.exception = None
def run(self):
try:
if self._target:
self._target(*self._args, **self._kwargs)
except Exception as e:
self.exception = e
def task(task_id):
if task_id % 2 == 0:
raise ValueError(f"Task {task_id} failed!")
return task_id
threads = [SafeThread(target=task, args=(i,)) for i in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
for i, t in enumerate(threads):
if t.exception:
print(f"Thread {i}: FAILED — {t.exception}")
else:
print(f"Thread {i}: succeeded")
```