Python Multithreading – Create, Sync, and Communicate Threads
Introduction – Why Use Multithreading in Python?
Python is known for its simplicity and readability, but did you know it also supports multithreading?
Multithreading is a powerful way to:
- Run multiple tasks concurrently
- Optimize I/O-bound operations
- Build responsive applications (e.g., GUI, network services)
Even with Python’s Global Interpreter Lock (GIL), multithreading is ideal for I/O-bound tasks like file I/O, network calls, or database operations.
In this guide, you’ll learn:
- Thread lifecycle in Python
- Creating, starting, joining threads
- Main thread, daemon threads, thread priorities
- Thread pools for scalable parallelism
- Synchronization, deadlocks, and inter-thread communication
Thread Lifecycle in Python
A Python thread goes through several states:
| State | Description |
|---|---|
| New | Thread is created but not started yet |
| Runnable | Thread is ready to run (started) |
| Running | Actively executing instructions |
| Blocked | Waiting for I/O or a lock |
| Terminated | Execution completed or stopped |
Python threads are managed by the OS scheduler; Python doesn’t expose all states explicitly.
Create / Start / Join Threads
Creating a Thread with threading.Thread
import threading
def task():
print("Thread is running")
t = threading.Thread(target=task)
t.start() # Starts the thread
t.join() # Waits for the thread to finish
Use join() to synchronize the main thread with the child thread.
Thread with Arguments
def greet(name):
print(f"Hello {name}")
t = threading.Thread(target=greet, args=("Alice",))
t.start()
t.join()
Using Subclassing
class MyThread(threading.Thread):
def run(self):
print("Thread via subclass")
t = MyThread()
t.start()
t.join()
Thread Pools with concurrent.futures
Managing many threads manually is hard. Thread pools offer a higher-level abstraction.
from concurrent.futures import ThreadPoolExecutor
def square(n):
return n * n
with ThreadPoolExecutor(max_workers=3) as executor:
results = executor.map(square, [1, 2, 3, 4])
print(list(results))
Output:
[1, 4, 9, 16]
Ideal for batch processing, parallel tasks, and web scraping.
Main Thread / Daemon / Priority
Main Thread
import threading
print("Main thread:", threading.current_thread().name)
Python automatically starts a main thread when your program begins.
Daemon Threads
Daemon threads run in the background and automatically terminate when the main program exits.
def background_task():
while True:
print("Running in background")
t = threading.Thread(target=background_task, daemon=True)
t.start()
Daemons are great for logging, heartbeat monitors, and cleanups.
⏫ Thread Priority
Python’s threading module does not support priority control directly. Thread scheduling is left to the OS.
If you need more control, consider multiprocessing or external libraries like psutil.
Synchronization and Deadlocks
Without synchronization, race conditions can occur when multiple threads modify shared data.
Using threading.Lock
lock = threading.Lock()
counter = 0
def safe_increment():
global counter
for _ in range(100000):
with lock:
counter += 1
Deadlock Example
lock1 = threading.Lock()
lock2 = threading.Lock()
def thread1():
with lock1:
with lock2:
print("Thread1 acquired both locks")
def thread2():
with lock2:
with lock1:
print("Thread2 acquired both locks")
These two threads could deadlock by waiting for each other’s lock.
Avoid nested locks or use threading.RLock() when needed.
Inter-thread Communication
Python threads share memory. Common communication tools include:
Shared Variables with Lock
queue = []
def producer():
with lock:
queue.append("data")
def consumer():
with lock:
if queue:
print(queue.pop(0))
queue.Queue for Thread-safe Messaging
import queue
q = queue.Queue()
def producer():
q.put("message")
def consumer():
msg = q.get()
print("Received:", msg)
threading.Thread(target=producer).start()
threading.Thread(target=consumer).start()
queue.Queue is thread-safe and handles blocking automatically.
Best Practices for Python Multithreading
| Do This | Avoid This |
|---|---|
Use threading.Lock() for shared data | Modifying shared variables without a lock |
Use ThreadPoolExecutor for parallel tasks | Creating hundreds of threads manually |
Use join() to wait for thread completion | Ignoring thread synchronization |
Use daemon=True only for background tasks | Relying on daemons for important work |
Summary – Recap & Next Steps
Python multithreading lets you build concurrent, responsive, and efficient applications—especially for I/O-bound tasks.
Key Takeaways:
- Use
threading.ThreadorThreadPoolExecutorfor concurrent tasks - Understand the thread lifecycle: create → start → run → terminate
- Use locks and queues for synchronization and communication
- Daemon threads die with the main program; regular threads don’t
- Deadlocks can occur—always design thread-safe logic
Real-World Relevance:
Used in web scrapers, network services, chat apps, logging systems, and asynchronous I/O operations.
FAQ – Python Multithreading
Can Python use multiple threads for CPU-bound tasks?
No. Due to the GIL, use multiprocessing for CPU-bound operations.
What is the difference between threading and multiprocessing?
threading= shared memory, ideal for I/O-bound tasksmultiprocessing= separate memory, ideal for CPU-bound tasks
What is a daemon thread?
A background thread that automatically terminates when the main program exits.
Can threads share data?
Yes. Threads share memory, but access must be synchronized.
What is the safest way to communicate between threads?
Use queue.Queue – it’s thread-safe and handles locking internally.
Share Now :
