Concurrency Primitives
threading Module
Section titled “threading Module”The threading module provides low-level thread primitives. Python threads are OS-level threads Managed by the platform’s native threading implementation (pthreads on Linux, Windows threads on Windows).
Creating Threads
Section titled “Creating Threads”import threadingimport time
def worker(thread_id, duration): print(f"Thread {thread_id} starting") time.sleep(duration) print(f"Thread {thread_id} finished after {duration}s")
threads = []for i in range(5): t = threading.Thread(target=worker, args=(i, i + 1)) threads.append(t) t.start()
for t in threads: t.join() # Wait for all threads to complete
print("All threads finished")Thread Subclassing
Section titled “Thread Subclassing”import threading
class WorkerThread(threading.Thread): def __init__(self, name, task_data): super().__init__(name=name) self.task_data = task_data self.result = None
def run(self): print(f"{self.name} processing {self.task_data}") self.result = f"processed_{self.task_data}"
t = WorkerThread("worker-1", "task-a")t.start()t.join()print(t.result) # processed_task-aDaemon Threads
Section titled “Daemon Threads”import threadingimport time
def daemon_worker(): while True: print("Daemon running") time.sleep(1)
def regular_worker(): time.sleep(3) print("Regular worker done")
d = threading.Thread(target=daemon_worker, daemon=True)r = threading.Thread(target=regular_worker)
d.start()r.start()
## When the main thread exits, daemon threads are killed immediately## Regular threads keep the process aliver.join() # Wait for regular thread# Program exits here — daemon thread is terminatedLock (Mutex)
Section titled “Lock (Mutex)”import threading
counter = 0lock = threading.Lock()
def increment(n): global counter for _ in range(n): with lock: counter += 1
threads = [ threading.Thread(target=increment, args=(100000,)), threading.Thread(target=increment, args=(100000,)),]
for t in threads: t.start()for t in threads: t.join()
print(f"Counter: {counter}") # Counter: 200000Without the lock, counter += 1 is not atomic — it involves reading, incrementing, and writing, Which can be interleaved between threads.
RLock (Reentrant Lock)
Section titled “RLock (Reentrant Lock)”import threading
lock = threading.RLock()
def outer(): with lock: print("outer acquired") inner()
def inner(): with lock: print("inner acquired — same thread, reentrant")
outer() # Works fine — RLock allows same thread to re-acquire| Feature | Lock | RLock |
|---|---|---|
| Reentrant | No (deadlocks) | Yes |
| Release by any thread | Yes | No (must be same thread) |
acquire() counting | No | Yes |
| Overhead | Lower | Slightly higher |
Semaphore
Section titled “Semaphore”import threadingimport time
semaphore = threading.Semaphore(3) # Allow 3 concurrent accesses
def access_resource(thread_id): print(f"Thread {thread_id} waiting...") with semaphore: print(f"Thread {thread_id} acquired, working...") time.sleep(2) print(f"Thread {thread_id} released")
threads = [threading.Thread(target=access_resource, args=(i,)) for i in range(6)]for t in threads: t.start()for t in threads: t.join()import threading
event = threading.Event()
def waiter(name): print(f"{name} waiting for signal") event.wait() print(f"{name} received signal, proceeding")
def setter(): time.sleep(2) print("Setting event") event.set()
w1 = threading.Thread(target=waiter, args=("W1",))w2 = threading.Thread(target=waiter, args=("W2",))s = threading.Thread(target=setter)
w1.start()w2.start()s.start()
w1.join()w2.join()s.join()Condition
Section titled “Condition”import threadingimport timeimport random
buffer = []buffer_lock = threading.Condition()MAX_SIZE = 5
def producer(): for i in range(10): with buffer_lock: while len(buffer) >= MAX_SIZE: buffer_lock.wait() item = f"item_{i}" buffer.append(item) print(f"Produced: {item}, buffer size: {len(buffer)}") buffer_lock.notify() time.sleep(random.random() * 0.1)
def consumer(): for _ in range(10): with buffer_lock: while len(buffer) == 0: buffer_lock.wait() item = buffer.pop(0) print(f"Consumed: {item}, buffer size: {len(buffer)}") buffer_lock.notify() time.sleep(random.random() * 0.1)
p = threading.Thread(target=producer)c = threading.Thread(target=consumer)p.start()c.start()p.join()c.join()Barrier
Section titled “Barrier”import threading
barrier = threading.Barrier(3)
def phase(thread_id): print(f"Thread {thread_id} in phase 1") barrier.wait() print(f"Thread {thread_id} in phase 2") barrier.wait() print(f"Thread {thread_id} done")
threads = [threading.Thread(target=phase, args=(i,)) for i in range(3)]for t in threads: t.start()for t in threads: t.join()multiprocessing
Section titled “multiprocessing”The multiprocessing module creates separate OS processes, each with its own Python interpreter and GIL. This is the primary way to achieve true parallelism for CPU-bound work in Python.
Basic Usage
Section titled “Basic Usage”import multiprocessing
def square(x): return x * x
if __name__ == "__main__": with multiprocessing.Pool(4) as pool: results = pool.map(square, range(10)) print(results) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]import multiprocessingimport time
def compute(n): total = 0 for i in range(n): total += i ** 2 return total
if __name__ == "__main__": start = time.time() processes = [] for i in range(4): p = multiprocessing.Process(target=compute, args=(10_000_000,)) processes.append(p) p.start() for p in processes: p.join() elapsed = time.time() - start print(f"4 processes: {elapsed:.2f}s")Shared Memory
Section titled “Shared Memory”import multiprocessing
def increment(counter, lock): for _ in range(100000): with lock: counter.value += 1
if __name__ == "__main__": counter = multiprocessing.Value("i", 0) # Signed integer lock = multiprocessing.Lock()
processes = [ multiprocessing.Process(target=increment, args=(counter, lock)) for _ in range(4) ]
for p in processes: p.start() for p in processes: p.join()
print(f"Counter: {counter.value}") # Counter: 400000Shared Array
Section titled “Shared Array”import multiprocessing
def fill_array(arr, start, end): for i in range(start, end): arr[i] = i * i
if __name__ == "__main__": arr = multiprocessing.Array("d", 100) # Array of doubles size = len(arr) chunk = size // 4
processes = [ multiprocessing.Process(target=fill_array, args=(arr, i * chunk, (i + 1) * chunk)) for i in range(4) ]
for p in processes: p.start() for p in processes: p.join()
print(list(arr[:10])) # [0.0, 1.0, 4.0, 9.0, 16.0, 25.0, 36.0, 49.0, 64.0, 81.0]import multiprocessing
def sender(conn): conn.send(["hello", "from", "sender"]) conn.close()
def receiver(conn): data = conn.recv() print(f"Received: {data}") conn.close()
if __name__ == "__main__": parent_conn, child_conn = multiprocessing.Pipe() p1 = multiprocessing.Process(target=sender, args=(child_conn,)) p2 = multiprocessing.Process(target=receiver, args=(parent_conn,))
p1.start() p2.start() p1.join() p2.join()concurrent.futures provides a high-level interface for asynchronously executing callables using Threads or processes.
ThreadPoolExecutor
Section titled “ThreadPoolExecutor”from concurrent.futures import ThreadPoolExecutor, as_completedimport time
def fetch_url(url): time.sleep(1) # Simulate network latency return f"fetched {url}"
urls = [f"http://example.com/page/{i}" for i in range(10)]
with ThreadPoolExecutor(max_workers=5) as executor: futures = {executor.submit(fetch_url, url): url for url in urls} for future in as_completed(futures): url = futures[future] try: result = future.result() print(result) except Exception as e: print(f"Error fetching {url}: {e}")ProcessPoolExecutor
Section titled “ProcessPoolExecutor”from concurrent.futures import ProcessPoolExecutorimport time
def cpu_bound(n): total = 0 for i in range(n): total += i ** 3 return total
start = time.time()with ProcessPoolExecutor(max_workers=4) as executor: results = list(executor.map(cpu_bound, [10_000_000] * 8))elapsed = time.time() - startprint(f"Elapsed: {elapsed:.2f}s, results: {results[:2]}...")submit vs map
Section titled “submit vs map”from concurrent.futures import ThreadPoolExecutor
def process(item): return item * 2
# map — simple, ordered, blocks until all completewith ThreadPoolExecutor(max_workers=3) as executor: results = list(executor.map(process, range(5))) print(results) # [0, 2, 4, 6, 8]
# submit — more control, unordered resultswith ThreadPoolExecutor(max_workers=3) as executor: futures = [executor.submit(process, i) for i in range(5)] for future in futures: print(future.result()) # May be out of orderException Handling
Section titled “Exception Handling”from concurrent.futures import ThreadPoolExecutor
def may_fail(x): if x == 3: raise ValueError(f"Cannot process {x}") return x * 2
with ThreadPoolExecutor(max_workers=3) as executor: futures = [executor.submit(may_fail, i) for i in range(5)] for future in futures: try: print(future.result()) except ValueError as e: print(f"Failed: {e}")# 0, 2, Failed: Cannot process 3, 6, 8queue Module
Section titled “queue Module”The queue module provides thread-safe FIFO, LIFO, and priority queues.
Queue (FIFO)
Section titled “Queue (FIFO)”import queueimport threading
q = queue.Queue(maxsize=10)
def producer(): for i in range(20): q.put(f"item_{i}") print(f"Produced item_{i}")
def consumer(): while True: item = q.get() print(f"Consumed {item}") q.task_done()
p = threading.Thread(target=producer)c = threading.Thread(target=consumer, daemon=True)
p.start()c.start()p.join()q.join() # Block until all items are processedLifoQueue
Section titled “LifoQueue”import queue
lq = queue.LifoQueue()lq.put(1)lq.put(2)lq.put(3)print(lq.get()) # 3 — last in, first outprint(lq.get()) # 2PriorityQueue
Section titled “PriorityQueue”import queue
pq = queue.PriorityQueue()pq.put((2, "medium priority"))pq.put((1, "high priority"))pq.put((3, "low priority"))
print(pq.get()) # (1, 'high priority')print(pq.get()) # (2, 'medium priority')print(pq.get()) # (3, 'low priority')- Serialization: Shows how to serialize and deserialize data for inter-process communication in multiprocessing scenarios.
- Async/Await: Explores the asyncio event loop and coroutines as an alternative concurrency model to threads and processes.
- File I/O: Demonstrates thread-safe file operations and how concurrency primitives protect shared file resources.
- Error Handling: Explains how to handle exceptions in concurrent code, including thread-safe error logging and recovery.