Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Мультипоток (Multithreading)
Мультипоток — это подход к параллельному выполнению кода, при котором один процесс содержит несколько потоков (threads), работающих одновременно в общем адресном пространстве. Каждый поток — это упрощённая версия процесса, которая имеет собственный стек вызовов, но делит с другими потоками одну кучу памяти и ресурсы.
Потоки vs Процессы
import threading
import multiprocessing
import time
def work(name):
for i in range(3):
print(f"{name} работает: {i}")
time.sleep(0.1)
# ПОТОКИ (threading) — общая память, лёгкие
thread1 = threading.Thread(target=work, args=("Поток-1",))
thread2 = threading.Thread(target=work, args=("Поток-2",))
thread1.start()
thread2.start()
thread1.join() # Ожидаем завершения
thread2.join()
# ПРОЦЕССЫ (multiprocessing) — отдельная память, тяжелее
process1 = multiprocessing.Process(target=work, args=("Процесс-1",))
process2 = multiprocessing.Process(target=work, args=("Процесс-2",))
process1.start()
process2.start()
process1.join()
process2.join()
GIL (Global Interpreter Lock) в Python
Пythonовский GIL — это взаимный замок, который позволяет только одному потоку одновременно выполнять bytecode интерпретатора. Это означает, что потоки в CPython не дают настоящий параллелизм для CPU-bound задач:
import threading
import time
def cpu_bound_task():
"""Задача, требующая CPU"""
total = 0
for i in range(50_000_000):
total += i
return total
# Последовательное выполнение
start = time.time()
cpu_bound_task()
cpu_bound_task()
print(f"Последовательно: {time.time() - start:.2f}s") # ~10 сек
# С потоками (медленнее из-за GIL!)
start = time.time()
t1 = threading.Thread(target=cpu_bound_task)
t2 = threading.Thread(target=cpu_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"С потоками: {time.time() - start:.2f}s") # ~10+ сек (медленнее!)
GIL НЕ влияет на I/O-bound задачи (сетевые запросы, чтение файлов), потому что во время ожидания I/O поток отпускает GIL:
import threading
import time
import requests
def fetch_url(url):
"""I/O-bound задача"""
response = requests.get(url, timeout=5)
return len(response.text)
urls = ["https://example.com"] * 4
# Последовательно
start = time.time()
for url in urls:
fetch_url(url)
print(f"Последовательно: {time.time() - start:.2f}s") # ~8 сек
# С потоками (намного быстрее!)
start = time.time()
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"С потоками: {time.time() - start:.2f}s") # ~2 сек
Практические примеры
Пример 1: Простой многопоточный скачиватель
import threading
from queue import Queue
def download_worker(queue, results):
"""Рабочий поток для скачивания"""
while True:
url = queue.get()
if url is None:
break
try:
# Имитация скачивания
result = f"Downloaded {url}"
results.append(result)
finally:
queue.task_done()
queue = Queue()
results = []
# Создаём 3 рабочих потока
workers = []
for i in range(3):
t = threading.Thread(target=download_worker, args=(queue, results))
t.start()
workers.append(t)
# Добавляем задачи в очередь
for url in ["http://example.com", "http://google.com", "http://github.com"]:
queue.put(url)
# Сигнализируем об окончании
for _ in workers:
queue.put(None)
# Ждём завершения
for t in workers:
t.join()
print(results)
Пример 2: Lock для защиты общих данных
import threading
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
self.lock = threading.Lock() # Мьютекс
def withdraw(self, amount):
"""Снять деньги (потокобезопасно)"""
with self.lock: # Ждём, пока другие потоки отпустят замок
if self.balance >= amount:
self.balance -= amount
return True
return False
def deposit(self, amount):
"""Положить деньги (потокобезопасно)"""
with self.lock:
self.balance += amount
account = BankAccount(1000)
def withdraw_many():
for _ in range(100):
account.withdraw(1)
threads = [threading.Thread(target=withdraw_many) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"Остаток: {account.balance}") # 500 (5 потоков × 100 снятий)
Проблемы мультипоточности
- Race Conditions — когда несколько потоков получают доступ к данным одновременно
- Deadlock — потоки ждут друг друга в бесконечном цикле
- Сложность отладки — поведение недетерминировано
Когда использовать потоки
- I/O-bound операции: сетевые запросы, работа с файлами, БД
- Асинхронные операции: фоновые задачи
- GUI приложения: чтобы не блокировать интерфейс
Альтернативы
Для CPU-bound задач лучше использовать:
- multiprocessing — отдельные процессы, обходит GIL
- asyncio — асинхронное программирование в одном потоке
- ProcessPoolExecutor — пул процессов для параллелизма
Мультипоток — мощный инструмент для I/O-bound операций, но требует осторожности с общими ресурсами и понимания ограничений GIL.