# Python Multithreading and Multiprocessing

Python provides two main modules for handling concurrent execution: `threading` and `multiprocessing`. Understanding the difference between them is crucial for writing efficient concurrent programs, especially due to Python's Global Interpreter Lock (GIL).

## The Global Interpreter Lock (GIL)

The GIL is a mutex that allows only one thread to hold the control of the Python interpreter. This means that even in a multi-threaded architecture with more than one CPU core, only one thread can execute Python bytecode at a time.

*   **I/O-bound tasks**: The GIL is released during I/O operations, so multithreading works well here.
*   **CPU-bound tasks**: The GIL becomes a bottleneck, so multiprocessing is preferred.

## Multithreading

The `threading` module is used for running multiple threads (tasks, function calls) at the same time. It is best suited for I/O-bound tasks (e.g., network operations, file I/O).

### Example

```python
import threading
import time

def print_numbers():
    for i in range(5):
        time.sleep(1)
        print(f"Number: {i}")

def print_letters():
    for letter in 'abcde':
        time.sleep(1)
        print(f"Letter: {letter}")

t1 = threading.Thread(target=print_numbers)
t2 = threading.Thread(target=print_letters)

t1.start()
t2.start()

t1.join()
t2.join()

print("Done!")
```

## Multiprocessing

The `multiprocessing` module allows you to create processes that run independently. Each process has its own Python interpreter and memory space, bypassing the GIL. This is ideal for CPU-bound tasks (e.g., heavy computations).

### Example

```python
import multiprocessing
import time

def square_numbers():
    for i in range(5):
        time.sleep(1)
        print(f"Square: {i * i}")

if __name__ == "__main__":
    p1 = multiprocessing.Process(target=square_numbers)
    p1.start()
    p1.join()
    print("Done!")
```

[[programming/python/python]]