Parallel Processing#
Overview#
The GIL (Global Interpreter Lock) in Python#
Python’s GIL allows only one thread to execute at a time in a single process, affecting CPU-bound threading performance.
Parallelism vs. Concurrency#
Parallelism: Performing multiple tasks simultaneously.
Concurrency: Managing multiple tasks at the same time but not necessarily simultaneously.
Challenges#
Data Race: Two threads access shared data simultaneously, causing inconsistent results.
Deadlock: Two or more threads wait indefinitely for resources held by each other.
Thread Safety: Writing code that functions correctly during simultaneous execution.
Distributed Parallel Processing with joblib#
joblib provides a simple interface for efficient parallel computing.
Parallel(n_jobs=-1) will automatically distribute the workload across all available CPU cores.
Here is a basic example, where we need to call time_intensive_method() several times. Doing this in parallel streamlines it.
from joblib import Parallel, delayed
import time
def time_intensive_method(n):
time.sleep(1)
return n
results = Parallel(n_jobs=-1)(delayed(time_intensive_method)(n) for n in range(5))
Parallel Processing with multiprocessing#
Note
Special considerations apply when using Jupyter or Windows for this. Check out
Bob Swinkels’s blog on the topic for details.
Creating Processes
from multiprocessing import Process
def worker(num):
print(f'Worker {num}')
processes = []
for i in range(5):
p = Process(target=worker, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
Parallelize the computation using Pool to create 4 worker processes. This distributes the function across them.
from multiprocessing import Pool
def square(x):
return x * x
with Pool(4) as pool:
result = pool.map(square, [1, 2, 3, 4])
Communication Between Processes#
Queue allows us to communicate between processes.
In this example, a new process starts with the queue as an argument. Inside the worker function, q.put('Hello from worker') adds a message to the queue.
from multiprocessing import Process, Queue
def worker(q):
q.put('Hello from worker') # Add message to the queue
if __name__ == '__main__':
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
print(q.get()) # Output: Hello from worker
p.join() # Ensures the worker process completes before exiting.
Synchronizing Processes#
Locks, Semaphores, Events, and Conditions
from multiprocessing import Process, Lock
def printer(item, lock):
with lock:
print(item)
lock = Lock()
items = ['A', 'B', 'C']
for item in items:
Process(target=printer, args=(item, lock)).start()
Async#
Async programming allows for concurrent code execution using coroutines.
import asyncio
async def say_hello():
print('Hello')
await asyncio.sleep(1)
print('World')
asyncio.run(say_hello())
Here is an example to perform asynchronous HTTP requests, which allows other tasks to proceed while waiting for the response.
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
html = await fetch(session, 'https://example.com')
print(html)
asyncio.run(main())
Additional Topics#
Swifter: Parallel Data Processing with Pandas (Github page)
Dask enables parallel computing with task scheduling and can handle datasets larger than memory.
Numba and CuPy for GPU parallel processing