Python Threading
Setup
import threading
from threading import Lock, Thread
from time import sleep
⚠️ Never name your file
threading.py— it will override the built-in module and cause a circular import error.
Creating and Starting a Thread
def run(content):
print(content)
thread1 = Thread(target=run, args=("Hello",)) # args must be a tuple
thread1.start() # activates the thread
target— the function the thread will run (no parentheses)args— a tuple of arguments to pass (add trailing comma for single-arg tuples:("value",))- The thread does nothing until
.start()is called
The Main Thread
Every Python program always has at least one thread: the main thread. Any threads you create are additional threads attached to the same Python process.
print(threading.active_count()) # number of currently active threads
Thread Execution Order is Not Guaranteed
Just because you start Thread 1 before Thread 2 doesn't mean Thread 1 finishes first. The OS scheduler decides which thread runs when.
thread1 = Thread(target=run, args=(1,))
thread2 = Thread(target=run, args=(2,))
thread1.start()
thread2.start()
# Output order: unpredictable
thread.join() — Waiting for a Thread
Blocks execution until the thread finishes:
thread1.start()
thread2.start()
thread1.join() # wait for thread1 to finish
thread2.join() # wait for thread2 to finish
print("Both done")
Waiting on multiple threads (prerequisites pattern)
def start_game(prerequisites):
print("Waiting to start game...")
for t in prerequisites:
t.join() # wait for each prerequisite thread
print("Game started!")
load_assets_thread = Thread(target=load_assets)
load_player_thread = Thread(target=load_player)
start_game_thread = Thread(target=start_game, args=([load_assets_thread, load_player_thread],))
load_assets_thread.start()
load_player_thread.start()
start_game_thread.start() # starts after others — join prevents it running too early
⚠️ You cannot
join()a thread before it has been started — always start threads before joining them.
Locks (Mutex)
A lock (also called a mutex — mutually exclusive flag) ensures only one thread can proceed past a certain point at a time.
mutex = Lock()
mutex.acquire() # wait until the lock is available, then claim it
# ... protected code ...
mutex.release() # release so another thread can acquire it
- If Thread 2 tries to
acquire()a lock already held by Thread 1, Thread 2 waits - Thread 2 can only proceed once Thread 1 calls
release()
Basic lock example
def t1(lock):
print("Starting T1")
lock.acquire()
sleep(1)
print("T1")
lock.release()
def t2(lock):
print("Starting T2")
lock.acquire()
sleep(1)
print("T2")
lock.release()
lock = Lock()
Thread(target=t1, args=(lock,)).start()
Thread(target=t2, args=(lock,)).start()
# Output: Starting T1, Starting T2, T1, T2 (in order)
Using Two Locks to Synchronize Thread Order
To guarantee alternating execution between two threads (e.g. print 1, 2, 3, 4, 5 in order from two threads):
def print_values(name, values, start_lock, end_lock):
for item in values:
start_lock.acquire()
print(item)
end_lock.release()
lock1 = Lock()
lock2 = Lock()
lock2.acquire() # pre-acquire so thread2 waits until thread1 releases it
thread1 = Thread(target=print_values, args=("T1", [1, 3, 5], lock1, lock2))
thread2 = Thread(target=print_values, args=("T2", [2, 4], lock2, lock1))
thread1.start()
thread2.start()
# Guaranteed output: 1 2 3 4 5
How it works:
- Thread 1 acquires lock1 (free) → prints → releases lock2 → Thread 2 can now run
- Thread 2 acquires lock2 (now free) → prints → releases lock1 → Thread 1 can now run
- They alternate without needing any sleep() delays
Deadlocks
A deadlock occurs when two (or more) threads are each waiting for the other to release a lock — so nothing can proceed.
# Thread 1 waits for Thread 2, Thread 2 waits for Thread 1
# Neither can finish → program hangs silently forever
- No error is raised — the program just freezes
- Avoid by carefully managing lock acquisition order
- Never have Thread A wait for Thread B while Thread B is also waiting for Thread A
Joining Threads at End of Program
Always join your threads at the end of a module to prevent unexpected interleaving if the module is imported:
t1 = Thread(target=run)
t2 = Thread(target=run)
t1.start()
t2.start()
t1.join() # ensures both threads finish before control returns
t2.join() # to wherever this module was imported from
Without joins, if this file is imported, the importing thread may execute before these threads finish — causing interleaved or out-of-order output.
Key Takeaways & Recap
| Concept | Summary |
|---|---|
Thread(target=fn, args=(...,)) |
Create a thread; args must be a tuple |
.start() |
Activates the thread — nothing runs until this is called |
.join() |
Blocks until the thread finishes |
threading.active_count() |
Returns number of currently active threads |
| Lock / Mutex | Ensures only one thread executes a section at a time |
.acquire() |
Claim the lock — waits if already held by another thread |
.release() |
Free the lock so another thread can acquire it |
| Two-lock pattern | Guarantees alternating thread execution without delays |
| Deadlock | Two threads waiting on each other — program hangs silently |
| Join at end | Always join threads at module end to prevent import-related race conditions |