Работа с потоками: 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
Практические советы по работе с потоками
- Не используйте потоки для CPU-bound задач — это замедлит программу.
- Всегда закрывайте потоки — иначе они останутся висеть в памяти.
- Избегайте излишних блокировок — это может привести к взаимоблокировке (deadlock).
- Используйте
ThreadPoolExecutorдля удобного управления пулом потоков:
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=4) as executor:
executor.map(download, urls)
print("Удобно и без утечек памяти! ✨")
Теперь вы знаете, как заставить потоки работать на вас! 🐍💡