threading Module Complexity
The threading module runs Python code in operating-system threads and provides the locks, conditions, semaphores, events and barriers that coordinate them. Every blocking call costs the time it spends blocked, written w below; the notes say what an operation does on top of that wait.
Complexity Reference
Size variables: t = operating-system thread start latency, w = time spent blocked, W = threads waiting on the primitive, T = live threads, p = barrier parties, s = thread stack size, f = the caller's own function, whose time and space are its own. A wait given a timeout that expires also removes itself from the primitive's waiter queue, which is the O(W) term in the wait rows.
Thread
| Operation |
Time |
Space |
Notes |
Thread() |
O(1) |
O(1) |
Builds the object; nothing runs, and enumerate() does not list it, until start() |
Thread.start() |
O(t) |
O(s) |
Returns once the new thread has started and stored its ident; s is set by stack_size() |
Thread.run() |
O(f) |
O(f) |
Calls the target once, or nothing without one; start() runs it in the new thread, a direct call in the caller's |
Thread.join() |
O(w) |
O(1) |
Immediate on a finished thread; a timeout returns with the thread still alive |
Thread.is_alive() |
O(1) |
O(1) |
|
Thread.name/ident/native_id/daemon |
O(1) |
O(1) |
ident and native_id are None until started |
Thread.getName()/setName()/isDaemon()/setDaemon() |
O(1) |
O(1) |
Deprecated aliases; each call also emits a DeprecationWarning |
Timer
| Operation |
Time |
Space |
Notes |
Timer() |
O(1) |
O(1) |
A Thread subclass; start() spawns the thread |
Timer.run() |
O(w + f) |
O(f) |
Waits the interval, then calls the function once |
Timer.cancel() |
O(1) |
O(1) |
Sets an event; a timer still waiting ends at once instead of after the interval, one already in its function finishes it |
Lock and RLock
| Operation |
Time |
Space |
Notes |
Lock() / RLock() |
O(1) |
O(1) |
|
Lock.acquire() |
O(w) |
O(1) |
Immediate when free; w = wait for the holder or the timeout |
Lock.release() |
O(1) |
O(1) |
Raises RuntimeError when the lock is not held |
Lock.locked() |
O(1) |
O(1) |
|
RLock.acquire() |
O(w) |
O(1) |
Immediate for the owner, which only increments a count |
RLock.release() |
O(1) |
O(1) |
Frees the lock only when the count returns to zero |
RLock.locked() |
O(1) |
O(1) |
Python 3.14+ |
Condition
| Operation |
Time |
Space |
Notes |
Condition() |
O(1) |
O(1) |
Wraps the lock given, or a new RLock |
Condition.acquire() / Condition.release() |
O(w) |
O(1) |
The underlying lock's own methods |
Condition.locked() |
O(1) |
O(1) |
Python 3.14+; the underlying lock's |
Condition.wait() |
O(w + W) |
O(1) |
Releases the lock, waits, re-acquires |
Condition.wait_for() |
O(w + W + c·f) |
O(f) |
c = predicate calls, one before waiting and one per wakeup |
Condition.notify() |
O(n) |
O(1) |
Wakes the n oldest waiters, n = 1 by default |
Condition.notify_all() |
O(W) |
O(1) |
|
Condition.notifyAll() |
O(W) |
O(1) |
Deprecated alias, plus a DeprecationWarning |
Semaphore and BoundedSemaphore
| Operation |
Time |
Space |
Notes |
Semaphore() / BoundedSemaphore() |
O(1) |
O(1) |
|
Semaphore.acquire() |
O(w + W) |
O(1) |
Immediate while the counter is positive |
Semaphore.release() |
O(n) |
O(1) |
Adds n to the counter and wakes up to n waiters |
BoundedSemaphore.release() |
O(n) |
O(1) |
As Semaphore.release(), but raises ValueError past the initial value |
Event
| Operation |
Time |
Space |
Notes |
Event() |
O(1) |
O(1) |
|
Event.set() |
O(W) |
O(1) |
Wakes every waiter |
Event.clear() |
O(1) |
O(1) |
|
Event.is_set() |
O(1) |
O(1) |
|
Event.isSet() |
O(1) |
O(1) |
Deprecated alias, plus a DeprecationWarning |
Event.wait() |
O(w + W) |
O(1) |
Immediate once set |
Barrier
| Operation |
Time |
Space |
Notes |
Barrier() |
O(1) |
O(1) |
|
Barrier.wait() |
O(w + W + p + f) |
O(f) |
The last of p parties runs the action, f, and wakes the others; a timeout that expires breaks the barrier |
Barrier.reset() / Barrier.abort() |
O(w + W) |
O(1) |
Wake every waiter; a party still waiting for the barrier to fill raises BrokenBarrierError. w = a running action, which holds the barrier's lock |
Barrier.parties/n_waiting/broken |
O(1) |
O(1) |
|
local
| Operation |
Time |
Space |
Notes |
local() |
O(1) |
O(1) |
|
local attribute access |
O(1) |
O(a) per thread |
Each thread that touches the object gets its own dict of a attributes; the first access in a thread also re-runs a subclass's __init__ with the original arguments, at that __init__'s own cost |
Module functions
| Operation |
Time |
Space |
Notes |
current_thread() |
O(1) |
O(1) |
One dict lookup |
main_thread() |
O(1) |
O(1) |
|
active_count() |
O(1) |
O(1) |
Two dict lengths |
enumerate() |
O(T) |
O(T) |
A new list of every live thread |
get_ident() / get_native_id() |
O(1) |
O(1) |
|
activeCount() / currentThread() |
O(1) |
O(1) |
Deprecated aliases, plus a DeprecationWarning |
settrace() / setprofile() |
O(1) |
O(1) |
Stores the hook for threads started afterwards; running threads are not touched |
settrace_all_threads() / setprofile_all_threads() |
O(T) |
O(1) |
Python 3.12+; also installs the hook in every running thread |
gettrace() / getprofile() |
O(1) |
O(1) |
|
stack_size() |
O(1) |
O(1) |
Returns the previous size and sets the one for threads started afterwards; no argument means the platform default, and 1 to 32 KiB raises ValueError |
excepthook() |
O(b) |
O(b) |
Called once per unhandled exception in a thread; b = the size of the traceback it prints |
ExceptHookArgs
| Operation |
Time |
Space |
Notes |
ExceptHookArgs() |
O(1) |
O(1) |
Four-field record |
Constants and exceptions
| Operation |
Time |
Space |
Notes |
TIMEOUT_MAX |
O(1) |
O(1) |
The largest timeout a wait that blocks accepts; larger values raise OverflowError |
ThreadError / BrokenBarrierError |
O(1) |
O(1) |
ThreadError is RuntimeError; BrokenBarrierError subclasses it |
Basic Threading
import threading
import time
# Define function - O(1)
def worker():
print("Worker running")
time.sleep(1) # O(1) - sleep
# Create thread - O(1)
thread = threading.Thread(target=worker)
# Start thread - O(t)
thread.start() # O(t) - thread startup
# Wait for completion - O(w)
thread.join() # O(w) - wait
Multiple Threads
import threading
def task(name):
print(f"Task {name}")
# Create threads - O(n) total
threads = []
for i in range(5):
t = threading.Thread(target=task, args=(i,)) # O(1)
threads.append(t)
t.start() # O(t)
# Wait all - O(n + sum of waits)
for t in threads:
t.join() # O(w) each
Thread Safety with Locks
import threading
# Shared resource
counter = 0
lock = threading.Lock()
# Unsafe
def increment_unsafe():
global counter
counter += 1 # Race condition!
# Safe - O(1) lock operations when uncontended
def increment_safe():
global counter
with lock: # O(w) - acquire, immediate when free
counter += 1
# O(1) - release on exit
def repeat(func, times):
for _ in range(times):
func()
threads = [threading.Thread(target=repeat, args=(increment_safe, 10_000)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 40000
Coordinating Threads
import threading
ready = threading.Event()
slots = threading.Semaphore(2)
finish = threading.Barrier(3)
order = []
def worker(name):
ready.wait() # O(w) - immediate once set
with slots: # O(w) - at most two workers inside at once
order.append(name)
finish.wait() # O(w + p) - the last arrival wakes the other two
threads = [threading.Thread(target=worker, args=(n,)) for n in "abc"]
for t in threads:
t.start()
ready.set() # O(W) - wakes every worker already waiting; later ones pass straight through
for t in threads:
t.join()
print(sorted(order)) # ['a', 'b', 'c']
Thread-Local Data
import threading
class Context(threading.local):
def __init__(self, user):
self.user = user # runs again in each thread that touches the object
context = Context("default")
def show():
print(context.user) # default - this thread's own copy
context.user = "worker"
thread = threading.Thread(target=show)
thread.start()
thread.join()
print(context.user) # default - the worker's assignment stayed in its thread
Version Notes
- Python 3.10+:
activeCount(), currentThread(), notifyAll(), isSet(), getName(), setName(), isDaemon() and setDaemon() emit DeprecationWarning
- Python 3.12+:
settrace_all_threads() and setprofile_all_threads() added
- Python 3.13+: the optional free-threaded build runs Python code in parallel without the GIL; the default build still holds the GIL while running bytecode, so only native code that releases it runs in parallel
- Python 3.14+:
RLock.locked() added; passing arguments to RLock() is deprecated
Best Practices
✅ Do:
- Use locks for shared resources
- Use context managers for locks
- Set daemon=True for background threads
❌ Avoid:
- Sharing mutable objects without locks
- CPU-bound tasks (use multiprocessing)
- Deadlocks (acquire locks in order)