Работа с потоками: threading, GIL и ограничения

Что такое потоки и зачем они нужны?

Потоки (threads) — это «лёгкие» процессы внутри одной программы 🧵. Они позволяют выполнять несколько задач одновременно, используя общую память. В Python модуль threading — ваш лучший друг для работы с потоками!

Зачем использовать потоки?
- Ускорить выполнение I/O-операций (запросы в сеть, чтение файлов)
- Создать отзывчивый интерфейс (например, GUI не зависает при долгих вычислениях)

import threading
import time

def slow_operation():
    print("Начали медленную операцию...")
    time.sleep(3)
    print("Закончили!")

# Создаём поток
thread = threading.Thread(target=slow_operation)
thread.start()  # Запускаем
print("Главный поток продолжает работать! 🚀")

GIL: Главный ограничитель потоков в Python

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

Как обойти GIL?
- Использовать multiprocessing вместо threading
- Переложить тяжёлые вычисления на C-расширения (например, NumPy)

# CPU-bound задача (GIL мешает)
def calculate():
    result = 0
    for i in range(1000000):
        result += i * i

threads = [threading.Thread(target=calculate) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print("Готово! Но было быстрее без потоков... 🤔")

Когда потоки — отличное решение

Потоки идеальны для I/O-bound задач (ожидание ответа от сервера, чтение с диска). Пока один поток «спит», другой может работать!

Пример: скачивание файлов

import requests

def download(url, filename):
    response = requests.get(url)
    with open(filename, 'wb') as f:
        f.write(response.content)
    print(f"Загружен {filename}")

urls = ["https://example.com/file1.jpg", "https://example.com/file2.jpg"]
threads = []

for i, url in enumerate(urls):
    thread = threading.Thread(target=download, args=(url, f"file_{i}.jpg"))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print("Все файлы загружены! 🎉")

Осторожно: общие ресурсы и блокировки

Потоки используют общую память, что может привести к состоянию гонки (race condition). Решение — Lock из модуля threading.

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:  # Блокируем доступ
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(4)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"Итоговое значение: {counter} (должно быть 400000)")

Потоки vs Мультипроцессинг: краткое сравнение

Параметр threading multiprocessing
GIL Есть 🚫 Нет ✅
Память Общая Изолированная
Запуск Быстрый Медленный
Подходит для I/O-bound задачи CPU-bound задачи

Для CPU-bound задач смотрите уроки Данилы Бежина про multiprocessing: https://www.youtube.com/@DanilaBezhin


Практические советы по работе с потоками

  1. Не используйте потоки для CPU-bound задач — это замедлит программу.
  2. Всегда закрывайте потоки — иначе они останутся висеть в памяти.
  3. Избегайте излишних блокировок — это может привести к взаимоблокировке (deadlock).
  4. Используйте ThreadPoolExecutor для удобного управления пулом потоков:
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=4) as executor:
    executor.map(download, urls)
print("Удобно и без утечек памяти! ✨")

Теперь вы знаете, как заставить потоки работать на вас! 🐍💡

Скрыть рекламу навсегда

🎥 YouTube: программирование простым языком

Канал, где я спокойно и по шагам объясняю сложные темы — без заумных терминов и лишней теории.

Подходит, если раньше «не заходило», но хочется наконец понять.

▶️ Смотреть курсы на YouTube