4 min read

Parallelism in Python 0-1

Benchmark of different paradigms for parallelism in python

We have heard a lot of times that python de facto is single threaded and not a multi processing friendly language.

What does it mean ?

  • Python itself is not inherently single-threaded; rather, the issue lies with the standard implementation of Python, known as CPython, which uses the Global Interpreter Lock (GIL).

What is GIL ?

  • It's like a security guard that only lets one thread (a small unit of a program) use Python resources at a time.
  • The GIL made it easier to design the Python interpreter (specifically CPython). With the GIL in place, data structures don't have to be locked individually, simplifying the internal implementation.
  • Python for a long time was single threaded, but the external libraries which interacted with python could use multi threads
  • When Python was invented most of the computers were single threaded and then GIL allowed certain optimisations that sped up this common use case.
graph LR; style A fill:#f9f,stroke:#333,stroke-width:2px; style B fill:#fc9,stroke:#f66,stroke-width:2px; style C fill:#9cf,stroke:#36f,stroke-width:2px; style D fill:#9cf,stroke:#36f,stroke-width:2px; style E fill:#ff9,stroke:#fc3,stroke-width:2px; A[Python Application] -->|Starts| B[Main Thread]; B --> C[Thread 1]; B --> D[Thread 2]; C -->|Requests GIL| E[GIL]; D -->|Requests GIL| E; E -->|Grants| C; E -.->|Queues| D; C -->|Releases GIL| E; E -->|Grants| D;

What are the work arounds ?

The four main libraries to bypass Python's GIL limitations are Multi-Processing, Threading, AsyncIO, and Concurrent Futures.

Workaround Use Case Advantage
๐Ÿ”„ Multi-processing CPU-bound tasks True parallelism, separate memory space
๐Ÿงต Threading I/O-bound tasks Efficient for I/O-bound tasks
โฉ AsyncIO I/O-bound, high-concurrency Single-threaded concurrency
๐Ÿค Concurrent Futures Both CPU and I/O-bound Abstraction over threading and multiprocessing

We will go through these one by one to understand what are the benefits of each approach and in the end benchmark


๐Ÿงต Async IO

  • AsyncIO is often used as a way to write concurrent code in Python without having to deal with the Global Interpreter Lock (GIL), especially for I/O-bound and high-level structured network code
  • AsyncIO bought the power of the event loop to Python, making it easier to handle lots of I/O tasks simultaneously without waiting
  • AsyncIO is ideal for I/O-bound tasks but not so much for CPU-bound tasks due to the single-threaded event loop
๐Ÿ’ก
Not all the python libraries by default have support for async IO, requests which is a famous python library doesn't support async IO and you would need to use a counter part like session / aiohttp to achieve better results

๐Ÿงต Threading Library

  • Unlike AsyncIO, multi-threading uses system-level threads and can take advantage of multiple CPUs and cores, but it does not eliminate the GIL constraint for CPU-bound code.
  • Multi-threading allows for concurrent code execution, but it is often hampered by the Global Interpreter Lock (GIL) in Python when dealing with CPU-bound tasks.
๐Ÿ’ก
When to use threading over Async IO ?
Threading is compatible with python libraries like requests while async IO would require you to work with compatible libraries.

๐Ÿ”„ Multi processing

  • This is true parallelism, where we spawn another process. It bypasses the limitations of GIL
  • Ideal for CPU-bound tasks like data processing, number crunching, and complex computations, where you need to maximise the use of multiple CPU cores for better performance.

๐Ÿค Concurrent Futures

The concurrent.futures library in Python is essentially a high-level wrapper around the threading and multiprocessing modules. It provides a clean and simplified API to execute functions asynchronously using either threads (ThreadPoolExecutor) or processes (ProcessPoolExecutor).


Benchmarking

Time to see which library outperforms others.

CPU Bound process

We decided to stress test on a simple CPU intensive process like calculating a factorial. As expected multi processing module here is better than the others which have similar performance

IO Intensive task

Next up was an IO bound task which encompassed scrapping 100 web pages.

This shows that asyncio and multi threading have similar performances while asyncio is a little better. Using asyncio with a library which doesn't support event loop is equivalent to running it synchronously.


Summary

  • For CPU bound processes we can go with multi processing.
  • For IO bound we can either use threading or asyncIO.
  • If the library you are dealing with is compatible with asyncIO go with asyncIO else threading

๐Ÿ‘‹