Привет, любитель Python!
Слышал о потоках, но чувствуешь себя немного неуверенно? Не волнуйся! Потоки в Python — это не про силу джедаев. Это хороший инструмент, который, кстати, вполне дружелюбен, если знать основные правила общения с ним. Правда, у потоков в Python есть свои нюансы, и часто можно услышать пугающее слово GIL. Но не спеши пугаться и бежать в сторону async-кода! Потоки в Python отлично работают в задачах ввода-вывода и могут здорово ускорить выполнение твоей программы, если применять их грамотно.
Эта статья — как раз для тех, кто хочет понять потоки с нуля: разберём, для чего они нужны, когда стоит их использовать, а главное — как не наломать дров.
Почему потоки? Где и когда?
Будем честны — если твоя задача упирается в вычислительные ресурсы (например, считать Мандельброта, запуская миллионы операций), Python-потоки тебе мало помогут. Тут скорее пригодится multiprocessing
, но для задач, где мы ожидаем много ввода-вывода, потоки вполне себе находка. Сетевые запросы, взаимодействие с базами данных, файловые операции — вот где Python-потоки, при правильном подходе, покажут себя во всей красе.
Итак, перед тем как идти к практике, небольшой чеклист для оценки:
-
Задача связана с вводом-выводом? Потоки — хорошее решение.
-
Много численных расчётов? Лучше идти к
multiprocessing
. -
Работаем с чужими библиотеками, где потокобезопасность под вопросом? Осторожнее, об этом мы тоже поговорим.
Основы создания потоков
Итак, начнем с простого: как вообще создать поток?
import threading import time def print_numbers(): for i in range(10): print(f"Number: {i}") time.sleep(0.5) # Создаем поток thread = threading.Thread(target=print_numbers) thread.start() # Основной поток продолжает выполнять свои задачи print("Поток запущен!") # Ждем, пока поток завершится thread.join() print("Поток завершен!")
Здесь создали поток thread
с помощью threading.Thread
, передав ему target
, то есть функцию, которая будет выполняться в новом потоке. Дальше — вызвали start()
, что дало старт потоку, а join()
остановил главный поток и дождался завершения потока.
Вроде всё просто, да? Но вот тут-то и начинаются подводные камни. Главное — потоки в Python имеют доступ к общей памяти, и это тот момент, где легко ошибиться, поймав себе баг или, того хуже, гонку данных.
А теперь реальный сценарий: представим, что несколько потоков пытаются изменить один и тот же объект одновременно.
import threading counter = 0 def increment(): global counter for _ in range(100000): counter += 1 threads = [threading.Thread(target=increment) for _ in range(5)] for thread in threads: thread.start() for thread in threads: thread.join() print(f"Counter value: {counter}")
Можно подумать, что counter
должен быть равен 500000
, но на практике результат будет непредсказуемым. Это так называемся гонка данных. И вот тут наступает время познакомиться с Lock
.
Lock
Когда несколько потоков обращаются к одному и тому же ресурсу, надо уметь их «запирать», иначе они начнут писать друг по другу. В Python для этого есть Lock
.
import 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(5)] for thread in threads: thread.start() for thread in threads: thread.join() print(f"Counter value: {counter}")
Здесь with lock
говорит потоку: «Подожди, пока не получишь доступ к lock
, и только тогда выполняй операцию». С lock
мы защищаем код от гонок данных и делаем его безопасным. Теперь counter
точно будет равен 500000
.
Queue для многопоточной обработки данных
Часто задачи не просто запускают отдельные потоки, но и передают между ними данные. Python предлагает для этого класс Queue
, который как раз-таки потокобезопасен.
import threading import queue import time def worker(q): while not q.empty(): task = q.get() print(f"Processing {task}") time.sleep(1) q.task_done() # Создаем очередь и заполняем задачами q = queue.Queue() for i in range(5): q.put(f"Task {i}") # Запускаем несколько потоков-воркеров threads = [threading.Thread(target=worker, args=(q,)) for _ in range(3)] for thread in threads: thread.start() # Ждем, пока все задачи не будут завершены q.join() print("All tasks are processed!")
Здесь запускаем три потока, которые берут задачи из очереди и выполняют их. Обрати внимание на q.join()
— он дожидается завершения всех задач в очереди, что упрощает контроль.
GIL
И тут мы возвращаемся к нашему слону в комнате — GIL. Global Interpreter Lock позволяет только одному потоку исполнять Python-код в конкретный момент времени. Этот механизм помогает сделать Python более стабильным, но серьёзно ограничивает производительность потоков.
Как бороться с GIL? Ответ простой — не бороться. Если твоя задача — это ввод-вывод, ты почти не почувствуешь влияние GIL. Но если тебе нужно много считать — лучше задуматься об asyncio
или multiprocessing
.
Пример скачивания страниц с использованием потоков
Соберем всё, что разобрали, в небольшой, но полезный пример. Представим, что нужно скачать несколько страниц и обработать их.
import threading import queue import requests import time def download_page(url, q): try: response = requests.get(url) q.put((url, response.text)) print(f"{url} downloaded") except Exception as e: print(f"Failed to download {url}: {e}") urls = ["https://example.com", "https://example.org", "https://example.net"] q = queue.Queue() threads = [threading.Thread(target=download_page, args=(url, q)) for url in urls] # Запуск потоков for thread in threads: thread.start() # Ожидание завершения потоков for thread in threads: thread.join() # Обработка результатов while not q.empty(): url, content = q.get() print(f"{url} has {len(content)} characters")
Этот код запускает поток для каждой страницы, качает её, складывает результат в очередь и после этого обрабатывает. Как видишь, на скачивание контента GIL почти не влияет, и потоки справляются с задачей гораздо быстрее, чем если бы мы запускали их последовательно.
Заключение
Итак, что можно вынести из всего этого? Потоки в Python — штука полезная, но не панацея. Они отлично работают с вводом-выводом, но не подойдут для интенсивных вычислений. Главное, помни про GIL и используй Lock
, если работаешь с общими данными.
Также в заключение рекомендую обратить внимание на открытые уроки по темам:
-
12 ноября: Асинхронность и потоки: в чем разница? Узнать подробнее
-
26 ноября: Основы визуализации данных в работе аналитика. Узнать подробнее
ссылка на оригинал статьи https://habr.com/ru/articles/857094/
Добавить комментарий