Мастерство многопоточности в Python: GIL объяснён с практическими примерами
Python предлагает и multiprocessing, и multithreading для ускорения рабочих нагрузок. Это руководство проведёт вас через создание надежных потоковых приложений, нюансы Global Interpreter Lock (GIL) и как избегать частых подводных камней: дедлоков и состояний гонки.
Что такое поток (Thread)?
Поток — лёгкая единица выполнения внутри одного процесса. Потоки разделяют память процесса, позволяя эффективный обмен данными, в то время как ОС планирует их для одновременного запуска на одном или нескольких ядрах CPU.
Что такое процесс (Process)?
Процесс — независимый экземпляр программы. Когда вы запускаете приложение — браузер или текстовый редактор — ОС создаёт отдельный процесс со своим кодом, данными и картой памяти.
Основы многопоточности в Python
Модуль threading предоставляет высокоуровневый API для управления потоками, в то время как устаревший _thread сохранён только для обратной совместимости. Потоки идеальны для I/O-bound задач, а CPU-bound работу лучше выполнять через модуль multiprocessing для обхода GIL.
- Поток: общая память, лёгкий.
- Процесс: изолированная память, тяжелее.
- GIL: мьютекс, защищающий выполнение Python-байткода.
Когда использовать многопоточность
Многопоточность сияет, когда нужно выполнять несколько I/O-нагруженных операций — сетевые запросы, файловый ввод-вывод, вызовы БД — одновременно. Правильно спроектированные потоки улучшают отзывчивость и пропускную способность без накладных расходов на спавн новых процессов.
Ключевые модули Python для треддинга
- _thread — низкоуровневый, устарел.
- threading — современный, богатый функционалом, рекомендуемый выбор.
Использование модуля threading
Модуль threading предоставляет класс Thread, примитивы синхронизации (Lock, RLock, Semaphore, Condition, Event, Barrier) и утилиты вроде activeCount() и enumerate(). Ниже — минимальный пример демонстрации создания потоков и синхронизации.
import time
import _thread
def thread_test(name, wait):
for i in range(4):
time.sleep(wait)
print(f"Running {name}")
print(f"{name} has finished execution")
if __name__ == "__main__":
_thread.start_new_thread(thread_test, ("First Thread", 1))
_thread.start_new_thread(thread_test, ("Second Thread", 2))
_thread.start_new_thread(thread_test, ("Third Thread", 3))
Запуск этого кода даёт перемежающийся вывод, демонстрирующий конкурентное выполнение.

Пояснение
- Импорт
timeи legacy_thread. - Определение
thread_testдля выполнения работы и логирования прогресса. - Запуск трёх независимых потоков через
start_new_thread.
Современный треддинг с threading.Thread
На практике вы наследуетесь от threading.Thread и переопределяете метод run(). Следующий пример повторяет предыдущее поведение, но использует высокоуровневый API.
import time
import threading
class ThreadTester(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name = name
self.delay = delay
def run(self):
for _ in range(4):
time.sleep(self.delay)
print(f"Running {self.name}")
print(f"{self.name} has finished execution")
if __name__ == "__main__":
threads = [
ThreadTester("First Thread", 1),
ThreadTester("Second Thread", 2),
ThreadTester("Third Thread", 3),
]
for t in threads:
t.start()
for t in threads:
t.join()
Вывод:

Пояснение
- Наследуемся от
Threadдля инкапсуляции поведения потока. - Переопределяем
__init__для хранения поток-специфичных данных. - Переопределяем
runдля определения задачи. - Создаём, стартуем и ждём (join) каждый поток.
Частые подводные камни треддинга
Дедлоки (Deadlocks)
Дедлок возникает, когда два или более потоков бесконечно ждут ресурсы, удерживаемые друг другом. Классическая иллюстрация: задача об обедающих философах — пять философов конкурируют за пять вилок, каждый нуждует двух вилок для еды. Если каждый берёт правую вилку первой, возникает циклическое ожидание, останавливающее весь прогресс.
Состояния гонки (Race Conditions)
Когда несколько потоков модифицируют общие данные без надёжной координации, итоговое состояние становится непредсказуемым. Например:
i = 0
for _ in range(100):
print(i)
i += 1
Запуск этого цикла в параллельных потоках может дать перемешанную последовательность, потому что инкременты перекрываются.
Синхронизация потоков
Модуль threading поставляет несколько примитивов синхронизации. Фундаментальный — Lock, гарантирующий эксклюзивный доступ к общему ресурсу.
import threading
lock = threading.Lock()
def worker(name):
for _ in range(5):
lock.acquire()
print(f"{name} acquired lock")
# критическая секция
lock.release()
if __name__ == "__main__":
t1 = threading.Thread(target=worker, args=("Thread A",))
t2 = threading.Thread(target=worker, args=("Thread B",))
t1.start(); t2.start()
t1.join(); t2.join()
Вывод демонстрирует сериализованный доступ к критической секции.

Другие инструменты синхронизации
- RLock — реентерабельная блокировка.
- Semaphore — контроль доступа к конечному числу ресурсов.
- Condition — позволяет потокам ждать специфических условий.
- Event — простой флаг для межпоточного сигналинга.
- Barrier — блокирует набор потоков, пока все не достигнут точки.
Понимание Global Interpreter Lock (GIL)
CPython использует GIL для сериализации выполнения байткода между потоками. Хотя это защищает внутренние структуры интерпретатора (например, подсчёт ссылок) от состояний гонки, оно также ограничивает параллелизм для CPU-bound задач на многоядерных системах.
- Когда поток стартует, он захватывает GIL.
- Если другому потоку нужен GIL, он блокируется до освобождения первым.
- I/O-bound потоки отпускают GIL во время блокирующих вызовов, позволяя другим выполняться.
- CPU-bound потоки удерживают GIL, мешая другим потокам выполнять Python-код.
Следовательно, многопоточность в Python даёт мало пользы для вычислительно тяжёлых нагрузок; multiprocessing — предпочтительный подход в таких случаях.
Почему существует GIL
Сборщик мусора CPython полагается на подсчёт ссылок. Без глобальной блокировки конкурентные обновления счётчиков ссылок привели бы к гонкам и повреждению памяти. Пообъектная блокировка ввела бы значительные накладные расходы, поэтому GIL был принят как простое, эффективное решение.
Практические выводы
- Используйте
threadingдля I/O-bound конкурентности. - Предпочитайте
multiprocessingдля CPU-bound параллелизма. - Всегда синхронизируйте общие данные с локками или высокоуровневыми примитивами.
- Осознавайте влияние GIL при масштабировании по ядрам.
- Наследуйтесь от
Threadи переопределяйтеrun()для чистого, поддерживаемого кода.
Python
- Освоение функции free() в C: практическое руководство и пример
- Мастерство метода strip() в Python: подробное руководство и практические примеры
- Мастерство метода str.count() в Python: как подсчитывать символы и подстроки с примерами
- Функция round() в Python: подробное объяснение с практическими примерами
- Освоение функции map() в Python: синтаксис, примеры и лучшие практики
- Python timeit(): измерение времени выполнения с практическими примерами
- Counter в collections Python: эффективное подсчёт, обновление и арифметические операции
- Python list.count(): экспертное руководство с практическими примерами
- Python list.index(): как находить позиции элементов — практические примеры
- Многопоточность в C#: Повышение производительности и эффективности