Может ли многопоточность быть запущена на разных ядрах процесса?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Многопоточность и GIL в Python
Это очень важный вопрос, потому что ответ неочевиден для новичков. Краткий ответ: нет, в CPython многопоточность НЕ может запуститься на разных ядрах из-за Global Interpreter Lock (GIL). Но есть нюансы.
Что такое GIL?
Global Interpreter Lock — это мьютекс в CPython, который защищает доступ к объектам Python. Только один поток может выполнять Python код одновременно, даже если у тебя 16-ядерный процессор.
import threading
import time
def cpu_bound_task():
"""Задача, зависящая от CPU (вычисления)"""
total = 0
for i in range(100_000_000):
total += i
return total
def single_thread():
"""Однопоточное выполнение"""
start = time.time()
cpu_bound_task()
cpu_bound_task()
return time.time() - start
def multi_thread():
"""Многопоточное выполнение — не будет быстрее!"""
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()
return time.time() - start
print(f"Однопоток: {single_thread():.2f}s") # ~5 сек
print(f"Многопоток: {multi_thread():.2f}s") # ~5 сек (не быстрее!)
Видишь? Многопоточность здесь не помогает вообще. Оба занимают одно и то же время.
Почему GIL существует?
GIL был создан для упрощения внутреннего управления памятью в CPython. Без него нужна была бы сложная система лок-фри структур данных или fine-grained locking, что замедлило бы весь интерпретатор. Это исторический выбор дизайна.
Когда многопоточность всё же помогает?
Многопоточность полезна для I/O-bound задач, потому что при ожидании I/O (сетевой запрос, чтение файла), Python отпускает GIL.
import threading
import time
import requests
def fetch_url(url):
"""Задача, зависящая от I/O"""
response = requests.get(url)
return response.status_code
def single_thread_io():
"""Однопоточно"""
start = time.time()
for url in [http://example.com] * 10:
fetch_url(url)
return time.time() - start
def multi_thread_io():
"""Многопоточно — будет НАМНОГО быстрее!"""
start = time.time()
threads = []
for url in [http://example.com] * 10:
t = threading.Thread(target=fetch_url, args=(url,))
t.start()
threads.append(t)
for t in threads:
t.join()
return time.time() - start
print(f"Однопоток I/O: {single_thread_io():.2f}s") # ~10 сек
print(f"Многопоток I/O: {multi_thread_io():.2f}s") # ~1 сек (в 10 раз быстрее!)
Здесь многопоточность экономит время, потому что пока один поток ждёт ответа от сервера, другие потоки могут выполняться.
Альтернативы GIL
1. Multiprocessing — разные процессы, разные GIL
from multiprocessing import Pool
def cpu_task(n):
return sum(range(n * 1_000_000, (n+1) * 1_000_000))
if __name__ == __main__:
with Pool(processes=4) as pool:
results = pool.map(cpu_task, range(4))
# Теперь работает параллельно на разных ядрах!
Multiprocessing создаёт отдельный процесс для каждого задания. Каждый процесс имеет собственный GIL, поэтому работает по-настоящему параллельно. Минус: большие overhead при создании процесса и сложность синхронизации.
2. Asyncio — асинхронное программирование
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return response.status
async def fetch_many():
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, http://example.com) for _ in range(10)]
return await asyncio.gather(*tasks)
# Работает на одном потоке, но с меньшим overhead, чем threading
asyncio.run(fetch_many())
Асинхронность более эффективна для I/O, чем многопоточность. Здесь нет context switching между потоками, только явная передача управления при await.
3. Python 3.13+ — без GIL
Na Python 3.13 добавлена экспериментальная поддержка выключения GIL. Это большой шаг, и скоро многопоточность станет действительно параллельной.
Практические рекомендации
- CPU-bound: используй multiprocessing или вообще C-расширения (Cython, NumPy)
- I/O-bound: threading или asyncio (asyncio быстрее)
- Реал-тайм обработка: распредели на разные процессы или используй другой язык (Go, Rust)
Вывод
Многопоточность в Python не может работать на разных ядрах для CPU-bound задач из-за GIL. Для I/O-bound задач потоки работают хорошо, потому что GIL отпускается при ожидании. Для истинного параллелизма нужно использовать multiprocessing.