1 The Problem
We want a task queue: one part of the program adds jobs (send an email, resize an image), and background workers pick them up and process them — so the main program never waits. It teaches the producer-consumer pattern with a thread-safe queue, the backbone of every background-job system.
2 How to Think About It
Think about producers and consumers, before any code:
3 The Build — explained part by part
Here is a complete task queue with worker threads. Each part is explained below.
import queue
import threading
import time
class TaskQueue:
def __init__(self, num_workers=2):
self.jobs = queue.Queue() # thread-safe queue
self.results = []
self.lock = threading.Lock()
for _ in range(num_workers):
threading.Thread(target=self._worker, daemon=True).start()
def _worker(self):
while True:
func, args = self.jobs.get() # waits until a job is available
result = func(*args)
with self.lock: # protect the shared results list
self.results.append(result)
self.jobs.task_done()
def add(self, func, *args):
self.jobs.put((func, args))
def wait(self):
self.jobs.join() # block until all jobs are done
def square(n):
time.sleep(0.1) # pretend it is slow work
return n * n
if __name__ == "__main__":
q = TaskQueue(num_workers=3)
for i in range(5):
q.add(square, i)
q.wait()
print("Results:", sorted(q.results))
_worker — each worker loops forever:
self.jobs.get() waits until a job appears, runs it, stores the result, and marks it done. get() blocking means idle workers use no CPU.daemon=True — worker threads die automatically when the main program exits, so we do not have to shut them down manually.
with self.lock: — the results list is shared, and appending from multiple threads at once could corrupt it. The lock ensures only one worker writes at a time.
add(func, *args) — producers call this to enqueue a job: the function to run and its arguments.
wait() / jobs.join() — block until every queued job has been processed.
task_done() in the worker is what makes join() know when all are finished.
threading.Lock.queue.get(), which blocks efficiently until a job arrives.4 Test & Prove Each Part
We test that all jobs get processed and produce the right results, using a real queue but a quick task.
import queue
import threading
class TaskQueue:
def __init__(self, num_workers=2):
self.jobs = queue.Queue()
self.results = []
self.lock = threading.Lock()
for _ in range(num_workers):
threading.Thread(target=self._worker, daemon=True).start()
def _worker(self):
while True:
func, args = self.jobs.get()
result = func(*args)
with self.lock:
self.results.append(result)
self.jobs.task_done()
def add(self, func, *args):
self.jobs.put((func, args))
def wait(self):
self.jobs.join()
def double(n):
return n * 2
def test_all_processed():
"""All 10 jobs are processed."""
q = TaskQueue(3)
for i in range(10):
q.add(double, i)
q.wait()
assert len(q.results) == 10
def test_correct_results():
"""Results are correct (order may vary)."""
q = TaskQueue(2)
for i in range(5):
q.add(double, i)
q.wait()
assert sorted(q.results) == [0, 2, 4, 6, 8]
def test_single_worker():
"""Even one worker processes everything."""
q = TaskQueue(1)
q.add(double, 21)
q.wait()
assert q.results == [42]
Run with pytest -v. We sort results before checking because workers finish in unpredictable order — a key insight about concurrency: you control what gets done, not the exact order.
5 The Interface
Flow
q.add(send_email, "alice@x.com") # returns now
# worker sends it in the background6 Run It & Automate It
python3 queue_worker.pyWatch 3 workers process 5 jobs in parallel — faster than doing them one by one.
Jenkins runs the tests automatically — each line explained below.
Results: [0, 1, 4, 9, 16]wait() hangs forever.get() needs a matching task_done(), or join() never returns.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Priorities. Use a PriorityQueue so urgent jobs run first. (Teaches: priority queues.)
- Retries. Re-queue jobs that fail. (Teaches: error handling in workers.)
- Persistence. Save the queue so jobs survive a restart. (Teaches: durable queues.)