Визуализируйте многопоточные программы Python с open source инструментом – VizTracer

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Специально к старту нового потока курса Fullstack-разработчик на Python, представляем небольшой авторский обзор кроссплатформенного инструмента визуализации многопоточных программ – VizTracer. У VizTracer 57 форков и 841 звезд на Github. Настраиваемые события, отчёты в HTML, детальная информация о функциях с их исходным кодом, простота применения, отсутствие зависимостей и малый оверхед превращают VizTracer в мастхэв Python-разработчика.


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

В Python вариантов конкурентности много. Самый распространённый, вероятно, – многопоточность, которой добиваются с помощью модуля threading и несколько процессов с помощью модулей myltiprocessing и subprocess, а также недавно появившийся способ – async из модуля asyncio. До появления VizTracer в инструментах, использующих эти техники в целях анализа программ, был пробел.

VizTracer – инструмент отслеживания и визуализации написанных на Python программ, который помогает логировать, отлаживать и профилировать код. Хотя он хорошо работает в однопоточных программах с одной задачей, польза от него в смысле конкурентных программ делает инструмент уникальным.

Попробуем с простой задачей

Определим, являются ли числа в массиве простыми, и вернём массив логических значений. Вот простое решение:

def is_prime(n):
    for i in range(2, n):
        if n % i == 0:
            return False
    return True

def get_prime_arr(arr):
    return [is_prime(elem) for elem in arr]

Выполним его нормально, в один поток, и задействуем VizTracer.

if __name__ == "__main__":
    num_arr = [random.randint(100, 10000) for _ in range(6000)]
    get_prime_arr(num_arr)

Выполняем команду:

viz_tracer my_program.py

Отчёт стека вызовов показывает, что выполнение кода заняло 140 мс, а большую часть времени заняло выполнение get_prime_arr.

Здесь на каждом элементе массива выполняется функция is_prime. Это ожидаемо и не интересно, если VizTracer вам знаком.

Теперь попробуем многопоточную программу

Сделаем то же самое в программе с несколькими потоками:

if __name__ == "__main__":
    num_arr = [random.randint(100, 10000) for i in range(2000)]
    thread1 = Thread(target=get_prime_arr, args=(num_arr,))
    thread2 = Thread(target=get_prime_arr, args=(num_arr,))
    thread3 = Thread(target=get_prime_arr, args=(num_arr,))

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

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

Если вы знакомы с Python Global Lock (GIL), то можете ожидать, что программа не станет быстрее: из-за оверхеда она выполняется немного дольше 140 мс, однако можно наблюдать конкурентность множества потоков:

Когда работал один поток (и много раз выполнял функции is_prime), другой поток был заморожен (одна функция is_prime). Позже они поменялись. Это произошло из-за GIL, и причина в том, что в Python нет настоящей многопоточности. Возможна конкурентность, но не параллелизм.

Попробуем поработать с multiprocessing

Чтобы достичь параллелизма, воспользуемся библиотекой multiprocessing. Вот новая версия кода с применением этой библиотеки:

if __name__ == "__main__":
    num_arr = [random.randint(100, 10000) for _ in range(2000)]
   
    p1 = Process(target=get_prime_arr, args=(num_arr,))
    p2 = Process(target=get_prime_arr, args=(num_arr,))
    p3 = Process(target=get_prime_arr, args=(num_arr,))

    p1.start()
    p2.start()
    p3.start()

    p1.join()
    p2.join()
    p3.join()

Чтобы запустить его с помощью VizTracer, понадобится дополнительный аргумент:

viztracer --log_multiprocess my_program.py

Вся программа занимает чуть больше 50 мс, задача завершается менее чем за те же 50 мс, то есть программа ускорилась приблизительно втрое. Чтобы сравнить производительность, напишем версию в несколько процессов:

Без GIL множество процессов достигают параллелизма, то есть множество функций is_prime выполняются параллельно.

Однако многопоточность в Python не бесполезна. Например, для вычислительно-интенсивных программ и программ с интенсивным вводом-выводом, при помощи sleep можно имитировать задержку ввода-вывода:

def io_task():
    time.sleep(0.01)

Попробуем в однопоточной программе с одной задачей:

if __name__ == "__main__":
    for _ in range(3):
        io_task()

.

Вся программа занимает 30 мс; ничего особенного. Теперь воспользуемся многопоточностью:

if __name__ == "__main__":
    thread1 = Thread(target=io_task)
    thread2 = Thread(target=io_task)
    thread3 = Thread(target=io_task)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

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

Теперь поработаем с asyncio

В Python попробовали ввести интересную функциональность – асинхронное программирование. Нашу задачу можно написать асинхронно:

import asyncio

async def io_task():
    await asyncio.sleep(0.01)

async def main():
    t1 = asyncio.create_task(io_task())
    t2 = asyncio.create_task(io_task())
    t3 = asyncio.create_task(io_task())

    await t1
    await t2
    await t3


if __name__ == "__main__":
    asyncio.run(main())

Поскольку asyncio – буквально однопоточный планировщик с задачами, вы можете использовать VizTracer вместе с этим планировщиком:

Всё те же 10 мс, но большинство отображаемых функций – нижележащая структура, которая, вероятно, не интересует пользователей. Чтобы решить эту проблему, можно использовать --log_async – флаг разделит задачу на части:

viztracer --log_async my_program.py

Теперь задачи пользователя намного яснее. Большую часть времени  выполняемых задач нет: единственное, что делает программа, – спит. Вот интересная часть:

На временной шкале показывается, когда задачи создавались и выполнялись. Task-1 была сопрограммой mail(), которая создаёт другие задачи. Задачи 2, 3 и 4 выполняли io_task и sleep, а потом ждали пробуждения. Как показывает график, задачи не перекрывают друг друга, поскольку программа однопоточная, и VizTracer визуализировал её так, чтобы она была понятной. Чтобы сделать код интереснее, добавим вызов time.sleep, чтобы заблокировать асинхронный цикл:

async def io_task():
    time.sleep(0.01)
    await asyncio.sleep(0.01)

Программа заняла намного больше времени (40 мс) и задачи заняли свои места в асинхронном планировщике. Это поведение очень полезно для диагностики проблем поведения и производительности в асинхронных программах.

Смотрите на происходящее с помощью VizTracer

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

Исходный код VizTracer открыт под лицензией Apache 2.0, поддерживаются все операционные системы – Linux , MacOS, Windows. Вы можете узнать больше о его функциях и увидеть исходный код в репозитории VizTracer на Github. А освоить разработку на Python и стать Fullstack-разработчиком — на нашем специализированном курсе.

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

  • Профессия Data Scientist

  • Профессия Data Analyst

  • Курс по Data Engineering

Другие профессии и курсы

ПРОФЕССИИ

  • Профессия Fullstack-разработчик на Python

  • Профессия Java-разработчик

  • Профессия QA-инженер на JAVA

  • Профессия Frontend-разработчик

  • Профессия Этичный хакер

  • Профессия C++ разработчик

  • Профессия Разработчик игр на Unity

  • Профессия Веб-разработчик

  • Профессия iOS-разработчик с нуля

  • Профессия Android-разработчик с нуля

КУРСЫ

  • Курс по Machine Learning

  • Курс "Machine Learning и Deep Learning"

  • Курс "Математика для Data Science"

  • Курс "Математика и Machine Learning для Data Science" 

  • Курс "Python для веб-разработки"

  • Курс "Алгоритмы и структуры данных"

  • Курс по аналитике данных

  • Курс по DevOps

Источник: https://habr.com/ru/company/skillfactory/blog/545530/


Интересные статьи

Интересные статьи

openGauss — система управления реляционными базами данных с открытым исходным кодом, созданная инженерами Huawei. Новая версия 1.0.1, которая стала доступна в октябре 2020 года, значитель...
Разработчики проекта Brython Python выпустили новый релиз, Brython 3.9. Сам проект позиционируется, как «Python для браузера». Он основан на Python 3 и выполняется на стороне брау...
Привет Хабр. После экспериментов с многим известной базой из 60000 рукописных цифр MNIST возник логичный вопрос, есть ли что-то похожее, но с поддержкой не только цифр, но и букв. Как оказалос...
Привет, Хабр! Позвольте представиться: меня зовут Сергей Агальцов, и я "программист по жизни". Это значит, что я давно уже IT-менеджер, а вовсе не программист по профессии, но программирование ...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...