Python
Threads
Queue
Concurrency
Multithreading

Python threads and queue example

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

In Python, threads and queues are commonly used together to build safe producer-consumer systems. Threads let several tasks make progress at the same time, while queue.Queue provides a thread-safe way to pass work items from producers to workers without hand-writing locks around shared lists.

This pattern is especially useful for I/O-bound jobs such as downloading files, reading sockets, or processing background tasks. It is much less useful for CPU-bound parallelism because the Global Interpreter Lock limits how much pure Python bytecode can run truly in parallel.

Why queue.Queue Matters

The main value of queue.Queue is not that it stores items. Any list can do that. Its real value is synchronized access. Producers can put work into the queue, consumers can get work out, and the queue manages the internal locking so threads do not corrupt shared state.

It also gives you useful coordination features:

  • 'get() blocks until work is available,'
  • 'task_done() marks an item as finished,'
  • 'join() lets the main thread wait until all queued work has been processed.'

That is the foundation of a clean threaded pipeline.

A Producer-Consumer Example

The example below starts three worker threads, feeds them jobs, and uses a sentinel value of None to tell each worker when to stop.

python
1import queue
2import threading
3import time
4
5
6def worker(name, jobs):
7    while True:
8        item = jobs.get()
9
10        if item is None:
11            jobs.task_done()
12            print(f"{name} exiting")
13            break
14
15        print(f"{name} processing job {item}")
16        time.sleep(0.5)
17        print(f"{name} finished job {item}")
18        jobs.task_done()
19
20
21job_queue = queue.Queue()
22threads = []
23
24for i in range(3):
25    thread = threading.Thread(target=worker, args=(f"worker-{i}", job_queue))
26    thread.start()
27    threads.append(thread)
28
29for job_id in range(8):
30    job_queue.put(job_id)
31
32# One sentinel per worker
33for _ in threads:
34    job_queue.put(None)
35
36job_queue.join()
37
38for thread in threads:
39    thread.join()
40
41print("All work complete")

A few details matter here. Each call to get() must eventually be matched with task_done(). The sentinel is placed once per worker so every worker gets a chance to exit. Finally, join() on the queue waits for all submitted items, including sentinels, to be acknowledged.

Choosing the Right Thread Model

This design works well when the task spends significant time waiting on external resources. For example, if each worker is reading from an API or writing files to disk, threads can hide that waiting time and improve overall throughput.

If the workload is CPU-heavy, threads are usually the wrong scaling strategy in CPython. In that case, multiprocessing or native code that releases the GIL is often a better option.

Bounded Queues and Backpressure

You can also give the queue a maximum size:

python
jobs = queue.Queue(maxsize=100)

That creates backpressure. If producers submit items faster than workers can consume them, put() will block once the queue fills up. This is useful when you want to prevent unbounded memory growth in long-running services.

Without a limit, a fast producer can flood the process with pending items even though workers are far behind.

Common Pitfalls

  • Forgetting task_done(). If you use queue.join(), one missing task_done() can make the program wait forever.
  • Using one sentinel for multiple worker threads. Only one worker exits, while the others keep blocking on get().
  • Sharing a plain list between threads instead of using queue.Queue, which invites race conditions and extra lock code.
  • Expecting Python threads to speed up CPU-bound work in CPython. They are mainly helpful for I/O-bound concurrency.
  • Not joining threads before process exit, which can leave background work incomplete or produce inconsistent shutdown behavior.

Summary

  • 'queue.Queue is the standard thread-safe handoff mechanism for Python worker threads.'
  • The producer-consumer pattern is a good fit for I/O-bound background work.
  • Use task_done() and join() to coordinate completion cleanly.
  • Send one sentinel per worker when it is time to stop.
  • For CPU-bound parallelism, prefer processes rather than threads in CPython.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.