Tutorials Logic, IN info@tutorialslogic.com

Python Threading: Run Waiting Tasks Concurrently

Python Threads

Threading lets a program manage multiple flows of work in the same process.

Threads are most useful for tasks that spend time waiting, such as network calls, file operations, or background monitoring.

Threading is not a magic speed boost for CPU-heavy Python code, and shared data needs careful design.

Start and Join

A Thread runs a callable in the same process and shares memory with the creating thread. Pass the callable itself to target, provide positional values through args, and call start exactly once. start schedules the work; calling run directly executes synchronously in the current thread.

join waits for completion and can take a timeout. After a timed join, check is_alive instead of assuming the worker stopped. Python cannot safely force an arbitrary thread to terminate, so long-running workers need cooperative cancellation through an Event or another shared signal.

  • Name threads so logs and diagnostic dumps identify the responsible worker.
  • Keep references to started threads when shutdown must join them.
  • Use daemon threads only for disposable background work; the interpreter can stop them abruptly at shutdown.
  • Capture worker failures explicitly because join does not re-raise an exception from the joined thread.

Run Two Workers

Run Two Workers
from threading import Thread

def worker(name):
    print(f"working: {name}")

threads = [
    Thread(target=worker, args=("download",)),
    Thread(target=worker, args=("report",)),
]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()
Output
working: download
working: report

The order may vary because threads run independently.

Shared State

Shared mutable state creates races when a result depends on the order of multiple operations. The GIL in a conventional CPython build does not turn a business operation such as read-modify-write into one atomic transaction, and free-threaded Python builds remove that implicit serialization. Write synchronization that expresses the real invariant.

A Lock protects a short critical section. Acquire it with a with statement so exceptions cannot leave it locked. A Queue is often the cleaner design: producers submit independent messages, consumers process them, and task_done plus join provide completion tracking without exposing a shared list.

  • Minimize the data protected by a lock and never hold a lock during slow network or file I/O.
  • Use RLock only when the same thread must re-enter the protected operation.
  • Use Event for broadcast state such as shutdown, Condition for a state predicate, and Semaphore for a concurrency limit.
  • Document which lock protects each shared invariant; relying on a built-in operation being atomic is not a portable design contract.

Independent Results

Independent Results
from threading import Thread

results = []

def collect(value):
    results.append(value.upper())

threads = [Thread(target=collect, args=(word,)) for word in ["csv", "json"]]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(sorted(results))
Output
['CSV', 'JSON']

Each thread processes one input, then the main program reads results after join.

Pass Work Through a Queue

queue.Queue provides synchronized put and get operations and can apply backpressure with maxsize. A fixed worker pool consumes items instead of creating an unbounded thread for every request. Send one sentinel per worker during shutdown so every consumer has a defined exit path.

Always pair each successful get with task_done in a finally block. Queue.join then waits until all submitted work has been acknowledged. Decide how failures are reported: a result queue, a Future from concurrent.futures, or a shared error collector guarded by a lock are all clearer than silently printing a traceback.

  • Bound the queue when producers must not outrun memory or an external service.
  • Choose a worker count from measured I/O latency and downstream capacity, not from input size alone.
  • Make tasks independently retryable when a transient failure can occur.

Coordinate Workers with Queue Sentinels

Coordinate Workers with Queue Sentinels
from queue import Queue
from threading import Thread

jobs = Queue()
results = Queue()

def worker():
    while True:
        value = jobs.get()
        try:
            if value is None:
                return
            results.put(value * value)
        finally:
            jobs.task_done()

workers = [Thread(target=worker, name=f"worker-{n}") for n in range(2)]
for thread in workers:
    thread.start()
for value in (2, 3, 4):
    jobs.put(value)
for _ in workers:
    jobs.put(None)

jobs.join()
for thread in workers:
    thread.join()
print(sorted(results.get() for _ in range(3)))
Output
[4, 9, 16]

The queue owns task coordination, sentinels stop both workers, and sorting makes the displayed result deterministic even though execution order is not.

Handle Cancellation and Worker Failures

A worker should check a threading.Event between bounded units of work. Blocking calls also need timeouts; otherwise a cancellation flag cannot be observed. Clearing an Event is appropriate only when the protocol supports restarting. For one-way shutdown, set it once and let ownership code join every worker.

Unhandled exceptions are passed to threading.excepthook, but production code usually needs the failure associated with its submitted task. ThreadPoolExecutor returns Future objects whose result method either returns the value or re-raises the worker exception in the caller. This makes bounded pools, cancellation, and error collection easier than managing raw threads for request-style work.

  • Set finite timeouts on network, queue, lock, and join operations where a permanent wait is unacceptable.
  • Log the thread name, task identity, exception type, and retry decision together.
  • Do not catch BaseException inside a worker; allow process-level signals such as KeyboardInterrupt to keep their intended meaning.

Choose Threads, Async Tasks, or Processes

Threads fit blocking I/O libraries and modest numbers of concurrent operations. asyncio fits many cooperative network tasks when the entire call chain has non-blocking APIs. Processes isolate memory and normally fit CPU-intensive Python work on conventional GIL-enabled CPython. A free-threaded CPython build can execute Python threads in parallel, but extension compatibility and synchronization still need testing.

Measure the real workload before choosing. Record throughput, latency, queue depth, CPU use, memory, and downstream rate limits. Concurrency can reduce waiting time while making overload, retries, shutdown, and observability harder; a sequential implementation remains the correct baseline for comparison.

  • Use ThreadPoolExecutor for finite independent calls that return values.
  • Use a long-lived Thread when it owns a loop, device, listener, or background lifecycle.
  • Use ProcessPoolExecutor for picklable CPU-bound functions when process overhead is acceptable.
  • Avoid mixing concurrency models until ownership and hand-off points are explicit.
Before you move on

Can You Use Python Threads?

5 checks
  • You can explain when threads help and when they do not.
  • You can start a thread with a target function.
  • You can wait for threads with join.
  • You can avoid careless shared-data changes.
  • You can keep concurrency separate from core business logic.

Thread Safety Traps

  • Expecting threads to speed up every program

    Use threads mainly for waiting-heavy work, not as a default for CPU-heavy loops.
  • Forgetting join

    Join threads when the main program needs their results before continuing.
  • Changing shared data freely

    Use safer patterns such as queues, locks, or independent result collection.

Try this next

Run Work Beside Work

0 of 3 completed

  1. Create two worker functions that print names and join both threads.
  2. Give each thread one word and collect uppercase results before printing sorted output.
  3. Write whether file downloads, number crunching, and API calls are good thread candidates.

Questions About Python Threads

They run concurrently, but exact scheduling depends on the interpreter and operating system. For beginner use, focus on waiting tasks.

Threads are scheduled independently, so one may print before another in different runs.

Learn normal functions first. Then use threading for simple concurrent waiting tasks and async for event-loop style I/O.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.