Как работает планировщик потоков в Python и какова роль GIL

«Как работает планировщик потоков в Python и какова роль GIL» — вопрос из категории Операционные системы, который задают на 10% собеседований Python Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Планировщик потоков в Python (CPython) — это стандартный планировщик операционной системы, но его работа тесно связана с Global Interpreter Lock (GIL).

Что такое GIL?

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

Почему он существует?

GIL упрощает управление памятью в CPython (автоматический подсчет ссылок становится потокобезопасным) и облегчает интеграцию с C-библиотеками, которые не являются потокобезопасными.

Механизм переключения потоков:

Поток, владеющий GIL, освобождает его, и планировщик передает управление другому потоку в следующих случаях:

  1. Операции ввода-вывода (I/O): При ожидании ответа от сети, диска или базы данных поток освобождает GIL, позволяя другим потокам выполняться.
  2. Явное освобождение: Вызов time.sleep().
  3. По истечении интервала: Поток принудительно освобождает GIL после выполнения определенного количества инструкций байт-кода (интервал можно получить через sys.getswitchinterval()). Это предотвращает «зависание» CPU-bound потоков.
import threading
import time

# Эта функция будет выполняться в потоке
def io_bound_task():
    print(f"{threading.current_thread().name} начал I/O операцию...")
    # Во время sleep() поток освобождает GIL, давая работать другим
    time.sleep(1)
    print(f"{threading.current_thread().name} завершил I/O операцию.")

# Создаем и запускаем два потока
thread1 = threading.Thread(target=io_bound_task, name="Поток 1")
thread2 = threading.Thread(target=io_bound_task, name="Поток 2")

thread1.start()
thread2.start()

thread1.join()
thread2.join()
# Вывод покажет, что потоки выполняются псевдо-параллельно

Вывод:

  • Для I/O-bound задач (ожидание сети, диска) threading эффективен, так как GIL освобождается во время ожидания.
  • Для CPU-bound задач (математические вычисления) threading не дает прироста производительности. Для настоящего параллелизма на многоядерных системах следует использовать модуль multiprocessing, где каждый процесс получает собственный интерпретатор и собственный GIL.