Рубрики
Без рубрики

Многопоточивый Python: Sliptering через узкое место ввода / вывода?

Автор оригинала: Victoria Drake.

Как воспользоваться параллелизмом в Python может сделать ваше программное обеспечение порядкам быстрее.

Я недавно разработал проект, который я позвонил Гидра : Многопоточная проверка ссылок, написанная в Python. В отличие от многих соседних участков Python, я обнаружил, что при исследовании Hydra использует только стандартные библиотеки без внешних зависимостей, таких как BeautifulSoup. Он предназначен для проведения процесса CI/CD, поэтому часть его успеха зависела от быстрой.

Несколько потоков в Python – это немного отвратительного предмета (не извините) в том, что интерпретатор Python на самом деле не позволяет выполнять несколько потоков одновременно.

Python’s Блокировка глобального интерпретатора или GIL, предотвращает несколько потоков с одновременно выполнять байтологи Python. Каждый поток, который хочет выполнить, должен сначала дождаться выпуске GIL на текущей выполнении потока. Гил в значительной степени микрофон в небольшой бюджетной конференц-панели, за исключением того, где никто не доходит до криков.

Это имеет преимущество предотвращения Гоночные условия Отказ Это, однако, не хватает преимуществ производительности, предоставляемых, используя несколько задач параллельно. (Если вы хотите преследовать параллелизм, параллелизм и многопоточность, см. параллелизм, параллелизм и многие потоки Санта-Клауса .)

Пока я предпочитаю пойти на свои удобные первоклассные примитивы, которые поддерживают параллелизм (см. Goroutines ), получатели этого проекта были более комфортными с Python. Я взял его как возможность проверить и исследовать!

Одновременно выполнение нескольких задач в Python не невозможно; Это просто занимает небольшую дополнительную работу. Для Hydra основное преимущество находится в преодолении узкого места ввода/вывода (ввода/вывода).

Чтобы проверить веб-страницы, Hydra должен выйти в Интернет и получить их. По сравнению с задачами, выполняемыми только процессором, выходя по сети, является сравнительно медленным. Как медленно?

Вот приблизительные сроки для задач, выполненных на типичном ПК:

Выполнить типичную инструкцию 1/1 000 000 000 наносец Процессор
извлекать из кэша кэша l1 0,5 наносец Процессор
Независимо от неправильной передачи 5 наносец Процессор
извлекать из кэш-памяти L2 7 наносец Процессор
Mutex Lock / разблокировка 25 наносец баран
привлекать от основной памяти 100 наносец баран
Отправить 2K BYTES в сети 1 Гбит / с 20 000 наносец Сеть
Читайте 1 МБ последовательно из памяти 250 000 наносец баран
извлекать из нового места диска (искать) 8 000 000 наносец (8 мс) Диск
Читайте 1 МБ последовательно с диска 20 000 000 наносец (20 мс) Диск
Отправить пакет нас в Европу и обратно 150 000 000 наносец (150 мс) Сеть

Питер Норвиг сначала опубликовал эти номера несколько лет назад в Научите себя программированием через десять лет Отказ Поскольку компьютеры и их компоненты меняют год в течение года, точные номера, показанные выше, не являются точкой. То, что эти цифры помогают иллюстрировать, это разница, по порядку величины между операциями.

Сравните разницу между Getching от основной памяти и отправка простого пакета через Интернет. В то время как оба эти операции возникают меньше, чем мгновение ока (буквально) с человеческой точки зрения, вы можете видеть, что отправка простого пакета через Интернет уже более миллион раз медленнее, чем вытеснение от оперативной памяти. Это разница, что в одной резьбовой программе может быстро накапливаться, чтобы сформировать хлопотные узкие места.

В HIDRA задача анализа данных реагирования и сборки результатов в отчет относительно быстро, поскольку все это происходит на процессоре. Медленная часть выполнения программы, более чем на шесть порядков, является задержкой сети. Не только Hydra нужно получить пакеты, но целые веб-страницы!

Одним из способов улучшения производительности Гидры является поиск способа для выполнения задач Page, не блокируя основной нить.

Python имеет пару вариантов выполнения задач параллельно: несколько процессов или несколько потоков. Эти методы позволяют обойти GIL и ускорить выполнение пару различных способов.

Несколько процессов

Чтобы выполнить параллельные задачи, используя несколько процессов, вы можете использовать Python’s ProcessPoolExecutor Отказ Бетонный подкласс Исполнитель от chapulent.futures модуль , ProcessPoolExecutor Использует пул процессов, порожденных с Многопроцессор модуль чтобы избежать Гиля.

Эта опция использует рабочие подпроцессы, которые максимально по умолчанию на количество процессоров на машине. Многопроцессор Модуль позволяет максимально распараллелизовать выполнение функций в процессах, которые действительно могут ускорить Compute-Criss (или CPU-CORCENT CORDES).

Поскольку основное узкое место для гидры является ввод/вывод, а не обработка, которая будет выполнена процессором, мне лучше служить с помощью нескольких потоков.

Несколько потоков

Скачать по имени, Python’s ThreadPoolexecutor Использует пул резьбы для выполнения асинхронных задач. Также подкласс Исполнитель , он использует определенное количество максимальных рабочих потоков (по крайней мере, пять по умолчанию, в соответствии с формулой мин (32, OS.cpu_count () + 4) ) и повторно простаивает резьбы перед началом новых, делая его довольно эффективный.

Вот фрагмент гидры с комментариями, показывающими, как Hydra использует ThreadPoolexecutor Для достижения параллельного многопотативного блаженства:

# Create the Checker class
class Checker:
    # Queue of links to be checked
    TO_PROCESS = Queue()
    # Maximum workers to run
    THREADS = 100
    # Maximum seconds to wait for HTTP response
    TIMEOUT = 60

    def __init__(self, url):
        ...
        # Create the thread pool
        self.pool = futures.ThreadPoolExecutor(max_workers=self.THREADS)


def run(self):
    # Run until the TO_PROCESS queue is empty
    while True:
        try:
            target_url = self.TO_PROCESS.get(block=True, timeout=2)
            # If we haven't already checked this link
            if target_url["url"] not in self.visited:
                # Mark it as visited
                self.visited.add(target_url["url"])
                # Submit the link to the pool
                job = self.pool.submit(self.load_url, target_url, self.TIMEOUT)
                job.add_done_callback(self.handle_future)
        except Empty:
            return
        except Exception as e:
            print(e)

Вы можете просмотреть полный код в Гидромагнитный репозиторий Github Отказ

Одиночная нить в многотехнику

Если вы хотите увидеть полный эффект, я сравнил время запуска для проверки моего веб-сайта между прототипом однопоточной программы, а также многопоточенным – я имею в виду многопоточность – Гидра.

time python3 slow-link-check.py https://victoria.dev

real    17m34.084s
user    11m40.761s
sys     0m5.436s


time python3 hydra.py https://victoria.dev

real    0m15.729s
user    0m11.071s
sys     0m2.526s

Однопоточная программа, которая блокирует ввод ввода/вывода, побежала примерно за семнадцать минут. Когда я впервые запустил многопоточную версию, она закончила в 1м13,358са – после некоторого профилирования и тюнинга, потребовалось немного до шестнадцати секунд.

Опять же, точное время не означает все это; Они будут варьироваться в зависимости от таких факторов, как размер выползания сайта, скорость вашей сети и баланс вашей программы между накладным расходом управления нитью и преимуществами параллелизма.

Чем важнее, и результатом я возьму в любой день, это программа, которая проходит несколько порядков быстрее.