A practical guide to multithreading in Python, demonstrating basic threading concepts, thread synchronization and thread-safe queue operations.
This repository contains three Python scripts that progressively introduce multithreading concepts:
- Basic Threading - Creating and managing multiple threads
- Thread Synchronization - Using locks to prevent race conditions
- Queue-based Threading - Producer-consumer pattern with thread-safe queues
Concept: Introduction to creating and managing multiple threads.
This script demonstrates the fundamental concepts of threading in Python:
- Creating Threads: Uses the
Threadclass to create 10 worker threads - Target Function: Each thread executes the
square()function, which performs calculations in a loop - Starting Threads: All threads are started using the
start()method - Joining Threads: The
join()method ensures the main program waits for all threads to complete before finishing
Key Takeaway: Multiple threads can execute concurrently, potentially reducing execution time for I/O-bound or independent tasks.
# Basic pattern
thread = Thread(target=function_name)
thread.start() # Begin execution
thread.join() # Wait for completionConcept: Preventing race conditions when multiple threads access shared data.
This script illustrates the critical problem of race conditions and how to solve them:
- The Problem: When multiple threads read and modify a shared variable (
database_value), race conditions can occur - The Solution: Using a
Lockobject to ensure only one thread can modify the shared variable at a time - Two Approaches:
- Manual:
lock.acquire()andlock.release() - Context Manager:
with lock:(recommended, automatically handles release)
- Manual:
Race Condition Example:
Without locks, if two threads read database_value = 0 simultaneously, both increment to 1, and both write back 1 (instead of the expected 2).
Key Takeaway: Always use locks when multiple threads need to modify shared data to ensure thread safety.
# Pattern for thread-safe data access
with lock:
# Critical section - only one thread at a time
shared_variable = modify(shared_variable)Concept: Producer-consumer pattern using Python's thread-safe Queue.
This script demonstrates a common multithreading pattern:
- Worker Threads: 10 daemon threads continuously process items from a queue
- Thread-Safe Queue: The
Queueclass handles all synchronization internally - Worker Pattern:
q.get()- Retrieves an item (blocks if queue is empty)- Process the item
q.task_done()- Signals completion of the item
- Main Thread: Acts as producer, adding 20 items to the queue
- Synchronization:
q.join()blocks until all items are processed - Daemon Threads: Automatically terminate when the main program exits
Key Takeaway: Queues provide a thread-safe way to distribute work among multiple threads without manual locking.
# Producer-consumer pattern
q = Queue()
# Worker threads
def worker(q):
while True:
item = q.get()
process(item)
q.task_done()
# Producer (main thread)
q.put(item)
q.join() # Wait for all items to be processedMultithreading allows a program to execute multiple operations concurrently within a single process. Each thread runs independently but shares the same memory space.
Benefits:
- Improved performance for I/O-bound operations
- Better resource utilization
- Responsive applications (UI remains active while background tasks run)
Challenges:
- Race conditions when accessing shared data
- Deadlocks if locks are not managed properly
- Debugging complexity
When multiple threads access shared resources, synchronization mechanisms are needed:
- Lock: Mutual exclusion - only one thread can hold the lock at a time
- Context Manager (
with lock:): Ensures lock is always released, even if an exception occurs
Python's Queue class provides built-in thread safety:
- No need for manual locking
- Blocks automatically when empty (get) or full (put)
- Perfect for producer-consumer scenarios
Each script can be run independently:
python thread.py
python data_sharing.py
python queue_in_thread.py- Python 3.x
- Standard library only (no external dependencies)
- Start with
thread.pyto understand basic thread creation - Move to
data_sharing.pyto learn about race conditions and locks - Finally, explore
queue_in_thread.pyfor practical worker patterns
- Use locks for shared mutable data - Prevents race conditions
- Prefer context managers -
with lock:is safer than manual acquire/release - Use queues for work distribution - Simpler and safer than manual synchronization
- Make threads daemon when appropriate - They automatically terminate with the main program
- Always call
task_done()- Required forqueue.join()to work correctly
- Python's Global Interpreter Lock (GIL) means threads don't provide true parallelism for CPU-bound tasks
- For CPU-intensive work, consider using
multiprocessinginstead - Threads are ideal for I/O-bound operations (network requests, file operations, database queries)
- This is my example code for learning purposes. Use freely.