> ## Content Index
> Fetch the complete content index at: https://articles.akadata.co.uk/llms.txt
> Use this file to discover other available public pages before exploring further.

# Python and True Multithreading Understanding Python's Multithreading Capabilities
- URL: https://articles.akadata.co.uk/python-and-true-multithreading-understanding-pythons-multithreading-capabilities/
- Published: 2025-02-23T15:40:55.000Z
- Updated: 2025-02-23T16:15:48.000Z
- Author: AKADATA

### Introduction

Python is often considered slow, but it does support multithreading and multitasking. Understanding how it achieves concurrency is essential to effectively leveraging its capabilities.

### Key Points About Python Threads:

- Python supports threads through the threading module.
- The Global Interpreter Lock (GIL) affects how Python executes threads by limiting the accurate parallel execution of Python bytecode.
- Python can request additional threads from the OS.
- Some C extensions release the GIL, allowing accurate parallel execution.

## Multithreading vs. Multiprocessing

- Multithreading in Python is helpful for I/O-bound tasks, such as network requests and file I/O.
- Multiprocessing is better suited for CPU-bound tasks, as it spawns separate processes, bypassing the GIL and utilizing multiple CPU cores.
- Some libraries, like NumPy and OpenCV, release the GIL, allowing for parallel execution in threads.

## Demonstrating Parallel Execution Across 32 Threads

To demonstrate how Python can effectively utilize multiple threads, we use a library that releases the GIL, such as NumPy, or leverages concurrent futures.ThreadPoolExecutor.

### Here's a testable Python script that:

- Performs a CPU-bound task (NumPy matrix multiplication) in 32 threads.
- Verifies parallel execution by measuring execution time.

```python
#!/usr/bin/env python

import concurrent.futures
import numpy as np
import time
import os

# Number of threads
NUM_THREADS = 32
MATRIX_SIZE = 500  # Adjust size for better benchmarking

# Function to perform a heavy computation
def matrix_multiply(_):
    A = np.random.rand(MATRIX_SIZE, MATRIX_SIZE)
    B = np.random.rand(MATRIX_SIZE, MATRIX_SIZE)
    return np.dot(A, B)  # NumPy releases the GIL during execution

def main():
    print(f"Running on {os.cpu_count()} cores...")

    # Single-threaded execution for baseline
    start = time.time()
    for _ in range(NUM_THREADS):
        matrix_multiply(None)
    single_thread_time = time.time() - start
    print(f"Single-threaded execution time: {single_thread_time:.2f} seconds")

    # Multi-threaded execution
    start = time.time()
    with concurrent.futures.ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
        results = list(executor.map(matrix_multiply, range(NUM_THREADS)))
    multi_thread_time = time.time() - start
    print(f"Multi-threaded execution time: {multi_thread_time:.2f} seconds")

    # Validate results
    assert len(results) == NUM_THREADS  # Ensures all tasks completed
    print("Parallel execution validated!")

if __name__ == "__main__":
    main()
```

## How This Works:

- Uses ThreadPoolExecutor with 32 threads.
- Runs NumPy matrix multiplication, which releases the GIL.
- Measures execution time for both single-threaded and multi-threaded runs.
- Validates that all 32 tasks are completed.

The code was first run in WSL2 (Python 3.10.12) and then Native Windows 11 Pro cmd and (Python 3.12)

![](https://articles.akadata.co.uk/content/images/2025/02/image-13.png)

## Expected Results:

- The multi-threaded version should be significantly faster if the threads execute in parallel.
- If GIL blocked execution, both runs would take roughly the same time.

With MATRIX\_SIZE = 5000 We get these results using all 32 threads on an I9-13900k and 128GB DDR5\. It sure appears to be using all cores! the ram usage rose from 39GB to 96GB memory usage during this time, I tried 50,000 however after around 20 minutes I reduced that to 5000, the results below. 

![](https://articles.akadata.co.uk/content/images/2025/02/image-15.png)

Next I changed threads to use 28, the actual core count of an I9-13900K leaving MATRIX\_SIZE at 5000 previously set and we see CPU saturation yet again.  

![](https://articles.akadata.co.uk/content/images/2025/02/image-16.png)

This demonstrates that Python can leverage accurate threading in specific scenarios, mainly when working with C-extensions that release the GIL. Understanding these nuances allows developers to optimize performance effectively.