Почему GIL не блокирует I/O-bound задачи в Python?

«Почему GIL не блокирует I/O-bound задачи в Python?» — вопрос из категории Асинхронность, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

GIL (Global Interpreter Lock) в Python — это механизм, который гарантирует, что только один поток Python может выполнять байт-код Python в любой момент времени. Это означает, что GIL блокирует выполнение CPU-bound задач в нескольких потоках на разных ядрах процессора.

Однако, при выполнении I/O-bound операций (таких как чтение/запись файлов, сетевые запросы, ожидание ввода пользователя), поток Python освобождает GIL. Это происходит потому, что I/O операции выполняются на уровне операционной системы (ОС) и не требуют активного участия интерпретатора Python. Пока ОС выполняет I/O операцию, поток Python переходит в состояние ожидания, и в этот момент GIL становится доступным для других потоков.

Почему это важно: Это позволяет другим потокам Python выполнять свой код (если он не I/O-bound) или также освобождать GIL для своих I/O операций, создавая иллюзию параллельного выполнения (конкурентности). Таким образом, для I/O-bound задач Python-потоки могут быть эффективны, так как время ожидания I/O не блокирует весь процесс.

Пример демонстрации освобождения GIL при I/O:

import threading
import time
import os

def io_task(task_id):
    print(f"Поток {task_id}: Начало I/O операции (PID: {os.getpid()})")
    time.sleep(2)  # Имитация блокирующей I/O операции (например, сетевой запрос)
    print(f"Поток {task_id}: Завершение I/O операции")

print("Запуск потоков...")
threads = []
for i in range(3):
    t = threading.Thread(target=io_task, args=(i,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()
print("Все потоки завершены.")

В этом примере, когда один поток вызывает time.sleep() (который имитирует ожидание I/O), он освобождает GIL. Это позволяет другим потокам начать свои time.sleep() почти сразу, что приводит к тому, что все три "I/O операции" завершатся примерно через 2 секунды, а не 6 секунд (если бы GIL блокировал их все время). Это демонстрирует конкурентное, а не истинно параллельное выполнение CPU-bound кода.