Правда ли, что python очень экономно потребляет оперативную память во время выполнения программы
Обновлено: 21.11.2024
В отличие от таких языков, как C, Python в большинстве случаев освобождает для вас память. Но иногда это не сработает так, как вы ожидаете.
Рассмотрите следующую программу на Python. Как вы думаете, сколько памяти она будет использовать при пиковых нагрузках?
Предполагая, что мы не можем изменить исходные данные, лучшее, что мы можем сделать, — это пиковый объем памяти в 2 ГБ: на короткий момент времени должны присутствовать как исходные 1 ГБ данных, так и измененная копия данных. На практике фактическое пиковое использование будет составлять 3 ГБ — ниже вы увидите реальный результат профилирования памяти, демонстрирующий это.
Лучшее, что мы можем сделать, это 2 ГБ, реальное использование — 3 ГБ. Откуда взялся этот дополнительный 1 ГБ использования памяти? Взаимодействие вызовов функций с управлением памятью Python.
Чтобы понять, почему и что можно сделать, чтобы исправить это, в этой статье будут рассмотрены:
- Краткий обзор того, как Python автоматически управляет памятью.
- Как функции влияют на отслеживание памяти в Python.
- Что вы можете сделать, чтобы решить эту проблему.
Как автоматическое управление памятью в Python упрощает вашу жизнь
В некоторых языках программирования вам нужно явно освободить всю выделенную память. Программа C, например, могла бы сделать:
Если вы не освободите вручную память, выделенную функцией malloc(), она никогда не будет освобождена.
Python, напротив, отслеживает объекты и автоматически освобождает их память, когда они больше не используются. Но иногда это не удается, и чтобы понять почему, нужно понять, как он их отслеживает.
В первом приближении реализация Python по умолчанию делает это с помощью подсчета ссылок:
- Каждый объект имеет счетчик мест, где он используется.
- Когда новое место/объект получает ссылку на объект, счетчик увеличивается на 1.
- Когда ссылка исчезает, счетчик уменьшается на 1.
- Когда счетчик достигает 0, память объекта освобождается, так как на него никто не ссылается.
Существуют дополнительные механизмы ("сборка мусора") для работы с циклическими ссылками, но они не имеют отношения к рассматриваемой теме.
Как функции взаимодействуют с управлением памятью Python
Один из способов добавить ссылку на объект — добавить ее к другому объекту: списку, словарю, атрибуту экземпляра класса и т. д. Но ссылки также создаются локальными переменными в функциях.
Давайте рассмотрим пример:
Допустим, мы вызываем f() и выполняем код шаг за шагом:
- Мы делаем obj = object() , что означает наличие локальной переменной obj, указывающей на созданный нами словарь. Эта переменная, созданная при запуске функции, увеличивает счетчик ссылок объекта.
- Затем мы передаем этот объект в g . Теперь есть локальная переменная с именем o, которая является дополнительной ссылкой на тот же словарь, поэтому общее количество ссылок равно 2.
- Затем мы печатаем o , что может добавить или не добавить ссылку, но как только функция print() завершит работу, у нас не будет дополнительных ссылок, и мы по-прежнему будем на уровне 2.
- g() возвращает значение, что означает, что локальная переменная o удаляется, уменьшая счетчик ссылок до 1.
- Наконец, функция f() возвращает значение, локальная переменная obj исчезает, и счетчик ссылок снова уменьшается до 0.
- Счетчик ссылок теперь равен 0, и словарь можно освободить. Это также уменьшает счетчик ссылок для строки «x» и созданного нами целого числа 1 по модулю некоторых оптимизаций, специфичных для строк и целых чисел, которые я не буду рассматривать.
Теперь давайте снова посмотрим на этот код на семантическом уровне. Как только словарь будет передан в g() , он никогда больше не будет использоваться f(), и тем не менее из f() все еще есть ссылка из-за переменной obj, поэтому счетчик ссылок равен 2. Локальный ссылка на переменную никогда не исчезнет, пока f() не завершит работу, даже если f() использует ее.
Теперь хранение небольшого словаря в памяти подольше не является проблемой. Но что, если этот объект использует много памяти?
Дополнительный 1 ГБ
Вернемся к нашему исходному коду, в котором мы неожиданно использовали дополнительный 1 ГБ памяти. Резюмируя:
Если мы профилируем его с помощью профилировщика памяти Fil, чтобы получать выделения во время пикового использования памяти, вот что мы получим:
В пиковые моменты мы используем 3 ГБ из-за трех распределений; в основном мы смотрим на момент времени, когда modify2() выделяет свой модифицированный массив:
- Исходный массив, созданный функцией load_1GB_of_data() .
- Первый измененный массив, созданный с помощью методаmodify1(); это зависает до тех пор, пока mod2() не завершит его использование, а затем не будет освобожден.
- Второй измененный массив, созданный с помощью методаmodify2() .
Проблема в том, что первое выделение: оно нам больше не нужно, как только mod1() создал модифицированную версию.Но из-за локальной переменной data в process_data() она не освобождается из памяти до тех пор, пока process_data() не вернется. А это означает, что использование памяти на 1 ГБ больше, чем могло бы быть в противном случае.
Решения: отпустить функции
Наша проблема в том, что process_data() слишком долго удерживает исходный массив:
Поэтому решения включают в себя обеспечение того, чтобы локальная переменная data не удерживала исходный массив дольше, чем это необходимо.
Если дополнительной ссылки нет, исходный массив можно удалить из памяти, как только он не будет использоваться:
Теперь нет ссылки на данные, поддерживающей исходный 1 ГБ данных, а пиковое использование памяти составит 2 ГБ.
Мы можем явным образом заменить данные результатом работы методаmodify1() :
Опять же, мы получаем 2 ГБ пиковой памяти, поскольку исходный массив можно освободить, как только завершится метод Modify1().
Это прием, заимствованный из C++: у нас есть объект, задачей которого является владение большим блоком данных объемом 1 ГБ, и мы передаем владельца вместо исходного объекта.
Хитрость заключается в том, что process_data() больше не ссылается на большой блок данных, а ссылается на владельца, а modify1 затем очищает/сбрасывает владельца после извлечения необходимых данных.
Отслеживание ссылок на объекты
В обычном коде то, что объекты живут немного дольше, не имеет значения. Но когда объект использует несколько гигабайт ОЗУ, слишком долгое существование может либо привести к нехватке памяти для вашей программы, либо потребовать оплаты дополнительного оборудования.
Поэтому заведите привычку мысленно отслеживать, где находятся ссылки на объекты. И если использование памяти слишком велико, а профилировщик предполагает, что проблема связана со ссылками на уровне функций, попробуйте один из методов, описанных выше.
Узнайте еще больше о способах сокращения использования памяти — прочтите остальную часть руководства по наборам данных, превышающим объем памяти, для Python.
Тратить время и деньги на процессы, использующие слишком много памяти?
Ваш пакетный процесс Python использует слишком много памяти, и вы понятия не имеете, какая часть вашего кода отвечает за это.
Вам нужен инструмент, который точно подскажет, на чем следует сосредоточить усилия по оптимизации, инструмент, разработанный для специалистов по обработке и анализу данных. Узнайте, чем может помочь профилировщик памяти Fil.
Как вы обрабатываете большие наборы данных с ограниченным объемом памяти?
Получите бесплатную памятку, в которой рассказывается, как обрабатывать большие объемы данных с ограниченным объемом памяти с помощью Python, NumPy и Pandas.
Кроме того, примерно каждую неделю вы будете получать новые статьи, в которых рассказывается, как обрабатывать большие данные и, в более общем плане, улучшать свои навыки разработки программного обеспечения, от тестирования до упаковки и повышения производительности:
- По возможности используйте отложенные вычисления (xrange вместо range)
- Удаление больших неиспользуемых объектов
- Использовать дочерние процессы, после их смерти ОС освобождает память
Сценарий находится на github здесь. Я также использую скрипт для периодической загрузки этих файлов на сервер. Оба эти скрипта довольно тривиальны. Кроме того, в системе больше ничего не работает, поэтому я чувствую, что в этих двух системах происходит только перехват памяти. Как лучше всего решить эту проблему. Я не очень хочу идти по пути подпроцесса.
Дополнительная информация:
- Сбор данных осуществляется на Raspberry Pi (512 МБ ОЗУ)
- Версия Python: 2.7
- Полное использование оперативной памяти занимает около 3-4 дней, после чего RaspberryPi зависает.
Я следил за этим руководством, чтобы узнать 20 программ, которые больше всего потребляют оперативную память.
Итак, два процесса Python потребляют часть оперативной памяти, но это очень мало по сравнению с общей потребляемой оперативной памятью. Ниже приведен вывод команды free.
Ниже приведен вывод команды top.
Как было предложено в первом ответе, я решил заглянуть в лог-файлы. Я просмотрел системный журнал, и вот результат хвоста на нем.
Эти сообщения заполняют лог-файлы и приходят каждую секунду. Интересно то, что я использую Ethernet, а не WiFi.
Таким образом, теперь непонятно, куда делась оперативка?
3 ответа 3
Большая часть вашей оперативной памяти свободна для приложений, поскольку она используется для буферов и кэширования. Посмотрите на строку "-/+ buffers/cache:", чтобы увидеть объем ОЗУ, который действительно используется/свободен. Объяснение можно найти здесь.
Чтобы убедиться, что в Python происходит утечка памяти, отслеживайте размер RSS (или %mem) этого Python с течением времени. Например. напишите сценарий оболочки, который вызывается из задания cron каждые пару часов, чтобы добавить вывод вашей цепочки команд ps и вывод команды free в файл.
Если вы обнаружите, что процессы Python производят утечку памяти, вы можете сделать несколько вещей:
- Измените свой скрипт так, чтобы он исчезал через 24 часа, и используйте, например, задание cron, чтобы перезапустить его (простой выход.)
- Углубленно изучите сам Python и особенно модули расширения, которые вы используете.Используйте модуль gc для мониторинга и влияния на использование памяти. Вы можете, например. регулярно вызывайте gc.count(), чтобы контролировать количество объектов, помеченных для сбора. Вы можете явно вызвать gc.collect() и посмотреть, уменьшит ли это использование памяти. Вы также можете изменить порог сбора.
Если использование оперативной памяти Python со временем не увеличится, это может быть еще одна программа демона. Сценарий ведения журнала памяти, о котором я упоминал выше, должен сказать вам, какой именно.
Возможна и другая причина зависания компьютера. Посмотрите файлы журналов Linux, чтобы найти подсказки.
Редактировать: Поскольку у вас есть wpa_supplicant, заполняющий файл журнала, вам следует проверить состояние файловой системы (систем). Полная файловая система может привести к зависанию системы. Если вы не используете беспроводной интерфейс, отключите его.
В 2015 году биоинформатик Йоханнес Кестер был, по его словам, "постоянным специалистом по Python". Он уже написал один популярный инструмент — менеджер рабочих процессов Snakemake — на языке программирования. Теперь он обдумывал проект, требующий такого уровня вычислительной производительности, которого Python просто не мог обеспечить. Поэтому он начал искать что-то новое.
Кестер, сейчас работающий в Университете Дуйсбург-Эссен в Германии, искал язык, который обладал бы «выразительностью» Python, но скоростью таких языков, как C и C++. Другими словами, «высокопроизводительный язык, который по-прежнему, скажем так, эргономичен в использовании», — объясняет он. То, что он нашел, было Rust.
Впервые созданный в 2006 году Грейдоном Хоаром в качестве побочного проекта во время работы в компании-разработчике браузеров Mozilla со штаб-квартирой в Маунтин-Вью, Калифорния, Rust сочетает в себе производительность таких языков, как C++, с более удобным синтаксисом, акцентом на безопасность кода и хорошей разработанный набор инструментов, упрощающих разработку. Части браузера Mozilla Firefox написаны на Rust, и, как сообщается, разработчики Microsoft используют его для перекодирования частей операционной системы Windows. Ежегодный опрос разработчиков Stack Overflow, в котором в этом году приняли участие около 65 000 программистов, уже 5 лет подряд признает Rust «самым любимым» языком программирования. Сайт для обмена кодом GitHub сообщает, что Rust был вторым самым быстрорастущим языком на платформе в 2019 году, что на 235 % больше, чем в предыдущем году.
Ученые тоже обращаются к Rust. Кестер, например, использовал его для создания приложения под названием Varlociraptor, которое сравнивает миллионы прочтений последовательностей с миллиардами генетических оснований для идентификации геномных вариантов. «Это огромные данные, — говорит он. «Так что это должно быть как можно быстрее». Но за эту мощь приходится платить: кривая обучения Rust крутая.
«Это требует некоторого времени на подготовку», — говорит Кэрол Николс, член основной команды Rust и основатель консалтинговой фирмы Integer 32 в Питтсбурге, штат Пенсильвания. «Но это дало мне возможность делать вещи, которые иначе я не смог бы сделать. Я вижу, что это время потрачено не зря».
Внимание: нет направляющих
В рабочих процессах для анализа научных данных обычно используются такие языки, как Python, R и Matlab. Они интерпретируют строки кода одну за другой, а затем выполняют их — стиль программирования, который хорош для изучения данных, но не на скорости.
C и C++ работают быстро, но у них «нет направляющих», — говорит Эшли Хаук, программист на Rust (или «растаец», как называют членов сообщества) из Стокгольма. Например, нет элементов управления, которые мешают программисту C или C++ неправомерно обращаться к памяти, которая уже была освобождена обратно в операционную систему, или предотвращают двойное освобождение одной и той же части программой. В лучшем случае это приведет к сбою программы. Но он также может возвращать бессмысленные данные или выявлять уязвимости в системе безопасности. По данным исследователей Microsoft, 70 % ошибок безопасности, которые компания исправляет каждый год, связаны с безопасностью памяти.
Правила памяти
Модель Rust использует правила для назначения каждой части памяти одному владельцу и ограничения доступа к ней. Код, нарушающий эти правила, никогда не сработает — он не скомпилируется. «У них есть система управления памятью, основанная на этой концепции времени жизни, которая позволяет компилятору отслеживать во время компиляции, когда память выделяется, когда она освобождается, кто ею владеет и кто может получить к ней доступ», — объясняет Роб Патро, специалист по вычислениям. биолог из Мэрилендского университета в Колледж-Парке. «Существует целый класс ошибок корректности, которые устраняются просто благодаря тому, как устроен язык».
Юлия: иди за синтаксисом, оставайся за скоростью
Эти же гарантии помогают обеспечить безопасную работу распараллеленного кода — программного обеспечения, написанного для работы на нескольких процессорах, например за счет устранения возможности одновременного доступа к одним и тем же данным нескольких вычислительных потоков.
В результате язык легче поддерживать и отлаживать, но сложнее в изучении. «Никаких других основных языков на самом деле нет этих концепций, и они действительно важны для понимания того, как вам нужно писать код на Rust», — говорит Николс. Стефан Хюгель, изучающий визуализацию географических данных в Тринити-колледже в Дублине, оценивает, что он потратил два или три месяца на портирование алгоритма Python для преобразования геопространственных координат из одной системы отсчета в другую в Rust, добившись четырехкратного ускорения выполнения. Ричард Аподака (Richard Apodaca), основатель компании по разработке программного обеспечения для химико-информационных систем Metaatomic из Ла-Хойи, Калифорния, говорит, что ему потребовалось около шести месяцев, чтобы освоить этот язык.
Сосредоточьтесь на удобстве использования
Чтобы компенсировать это, разработчики Rust оптимизировали взаимодействие с пользователем, — говорит Маниш Горегаокар, руководитель группы разработки инструментов Rust из Беркли, Калифорния. Например, компилятор выдает особенно информативные сообщения об ошибках, даже выделяя проблемный код и предлагая, как его исправить. «Если ваш язык будет представлять новую концепцию, с ним должно быть приятно работать», — объясняет Горегаокар.
Сообщество Rust также предоставляет обширную документацию и онлайн-справку, в том числе популярный онлайн-справочник под названием «Книга» и «Поваренную книгу» с рецептами решения распространенных проблем. Пользователи хвалят набор инструментов Rust — приложения, которые программисты используют для превращения кода в приложения (см. «Давайте окислим»). «Инструменты и инфраструктура вокруг Rust действительно феноменальны», — говорит Патро. В отличие от многих компиляторов и вспомогательных утилит, которые программисты используют для создания кода C, пользователи Rustace могут использовать один инструмент под названием Cargo для компиляции кода Rust, запуска тестов, автоматического создания документации, загрузки пакета в репозиторий и многого другого. Он также автоматически загружает и устанавливает сторонние пакеты. Подключаемый модуль Cargo под названием Clippy отмечает распространенные ошибки и «неидиоматический» код Rust — функцию, которую Патро называет «абсолютно феноменальной».
Приступаем к окислению
Вот как создать программу чтения файлов GenBank, чтобы вы могли изучить некоторые функции Rust.
• Выполните «грузовой запуск» из командной строки, чтобы загрузить внешние зависимости и создать приложение. По умолчанию приложение анализирует файл GenBank «nc_005816.gb» в репозитории GitHub, но вы можете указать альтернативный входной файл с «cargo run»
• Выполните включенные тесты с помощью «грузового теста».
• Создавайте и просматривайте документацию с помощью команды «cargo doc --open».
Существуют подключаемые модули Rust для популярных сред разработки, таких как Visual Studio Code от Microsoft и IntelliJ от JetBrains, а также «игровая площадка» Rust, предоставляющая живую онлайн-среду Rust для экспериментов с кодом. А Дэвид Латтимор, разработчик программного обеспечения из Сиднея, Австралия, создал «ядро» для использования Rust в вычислительных блокнотах Jupyter, а также интерактивную среду в стиле Python, называемую REPL (цикл чтения-оценки-печати).
Разработке помогает экосистема Rust, состоящая из сторонних пакетов, или «ящиков», которых в настоящее время насчитывается почти 50 000 (см. «Расцвет Rust»). Они инкапсулируют алгоритмы в таких дисциплинах, как биоинформатика (Köster’s Rust-Bio), геонауки (проект Geo-Rust) и математика (налгебра). Тем не менее, говорит Николс, «это определенно может склонить чашу весов в сторону от Rust, если нужных вам библиотек просто нет в Rust». Однако иногда программисты могут преодолеть этот пробел, используя «интерфейс внешних функций» Rust.
Окисленный код
Помимо логистики кодирования, нельзя отрицать, что Rust работает быстро. В мае биоинформатик Хенг Ли из Института рака Дана-Фарбер в Бостоне, штат Массачусетс, протестировал несколько языков в задаче вычислительной биологии, которая включала анализ 5,7 миллиона записей последовательностей. Rust вытеснил C и занял первое место. «Если мы хотим написать высокопроизводительную программу с использованием нескольких потоков, а также если вам нужно, чтобы она была очень быстрой и компактной в памяти, тогда Rust — идеальный выбор», — говорит Ли.
Луиз Ирбер, биоинформатик из Калифорнийского университета в Дэвисе, использовал Rust для перекодирования (или «окисления», на языке Rust) инструмента под названием Sourmash, который выполняет геномный поиск и таксономическое профилирование, чтобы упростить обслуживание программного обеспечения, получить доступ к современным языковым функциям и заставить код работать в веб-браузере, – говорит он.
Под руководством аспиранта Хирака Саркара команда Патро использовала Rust для создания инструмента анализа экспрессии генов под названием Terminus после того, как член команды Ави Шривастава вернулся после стажировки в 10x Genomics, биотехнологической компании в Плезантоне, Калифорния, которая использует Rust для разработки инструменты с открытым исходным кодом. «Прелесть Rust в том, что он очень упрощает задачу отладки, потому что управление памятью стало намного лучше», — объясняет Шривастава, который сейчас работает в Нью-Йоркском центре генома.
Но для многих рустообразных человеческий фактор не менее важен. Хаук, член сообщества ЛГБТ+, говорит, что пользователи Rust сделали все возможное, чтобы она чувствовала себя желанной. По ее словам, сообщество «всегда стремилось быть максимально инклюзивным — например, очень хорошо осознавать, как разнообразие влияет на вещи; хорошо осведомлены о том, как написать кодекс поведения и обеспечить соблюдение этого кодекса поведения».
«Вероятно, это основная причина, по которой я до сих пор пишу на Rust», — говорит Хаук. «Это потому, что сообщество такое фантастическое».
Природа 588, 185–186 (2020)
Обновления и исправления
Исправление от 2 декабря 2020 г. В более ранней версии этой статьи указан неправильный источник изображения.
Исправление от 11 декабря 2020 г. В более ранней версии этой функции ошибочно указывалось, что подключаемый модуль Clippy является сторонним компонентом.
Статьи по теме
Темы
Последние:
Темы
Последние:
Темы
Последние:
Машинное обучение и телефонные данные могут улучшить адресность гуманитарной помощи
Статья 16 22 марта
Рассеивание безбатарейных беспроводных устройств ветром
Статья 16 22 марта
Восстановление и атрибуция древних текстов с помощью глубоких нейронных сетей
Статья 09, 22 марта
Ответ на: Модели течения через губки должны учитывать ткань губки
Вопросы, возникшие 23 марта – 22 марта
ИИ читает по-гречески, катастрофа COVID — неделя в инфографике
Эволюция, эволюционируемость и инженерия ДНК, регулирующей гены
Статья 09, 22 марта
Где кибервойна России? Исследователи расшифровывают его стратегию
Объяснение новостей 17 – 22 марта
Избавьтесь от тирании копирования и вставки с помощью этих инструментов кодирования
Технологическая функция 28 – 22 февраля
Как исправить ошибки научного кодирования
Технологическая функция 31 – 22 января
Научный сотрудник в области накопления и преобразования энергии
Оксфордский центр перспективных исследований Сучжоу
Научные сотрудники (постдоки) в области криптографии
Междисциплинарный центр безопасности, надежности и доверия (SnT), Люксембургский университет
Как и в большинстве языков программирования, в Python также есть потоки. Код выполняется последовательно, а это означает, что каждая функция ожидает завершения предыдущей функции, прежде чем она сможет выполниться. В теории это звучит великолепно, однако во многих случаях в реальном мире это может стать узким местом.
Например, рассмотрим веб-приложение, которое отображает изображения собак из нескольких источников. Пользователь может просмотреть, а затем выбрать столько изображений, сколько он хочет загрузить. С точки зрения кода это будет выглядеть примерно так:
Это кажется довольно простым, не так ли? Пользователи выбирают список изображений, которые они загружают в последовательном порядке. Если загрузка каждого изображения занимает около 2 секунд, а изображений 5, время ожидания составляет приблизительно 10 секунд. Когда изображение загружается, это все, что делает ваша программа: просто подождите, пока оно загрузится.
Но маловероятно, что это единственный процесс, запущенный на компьютере пользователя. Они могут слушать песни, редактировать изображения или играть в игры. Кажется, что все это происходит одновременно, но на самом деле компьютер быстро переключается между задачами. По сути, каждый процесс, выполняемый компьютером, разбивается на фрагменты, которые ЦП оценивает, ставит в очередь и решает, когда обрабатывать. Существуют различные порядки обработки, которые может использовать ЦП (но давайте оставим это для другой статьи), чтобы оптимально обрабатывать каждый фрагмент настолько быстро, что создается впечатление, что компьютер выполняет несколько задач одновременно. Однако на самом деле это происходит одновременно.
Вернемся к нашему примеру с изображениями собак. Теперь, когда мы знаем, что компьютер пользователя может выполнять несколько задач одновременно, как мы можем ускорить загрузку? Ну, мы можем сказать процессору, что каждая загрузка образа может происходить одновременно, и что один образ не должен ждать завершения другого. Это позволяет загружать каждое изображение в отдельном «потоке».
Поток — это просто отдельный поток выполнения. Многопоточность — это процесс разделения основной программы на несколько потоков, которые процессор может выполнять одновременно.
Многопоточность и многопроцессорность
По своей структуре Python является линейным языком. По умолчанию он не использует преимущества нескольких ядер ЦП или графического процессора, но его можно настроить для этого. Первый шаг — понять разницу между многопоточностью и многопроцессорностью. Простой способ сделать это — связать задачи, связанные с вводом-выводом, с многопоточностью (например, такие задачи, как чтение/запись диска, вызовы API и работа в сети, которые ограничены подсистемой ввода-вывода), и связать задачи, связанные с процессором, с многопроцессорностью. (например, такие задачи, как обработка изображений или анализ данных, которые ограничены скоростью процессора).
Во время обработки задач ввода-вывода ЦП простаивает. Threading использует это время простоя для обработки других задач. Имейте в виду, что потоки, созданные из одного процесса, также используют одну и ту же память и блокировки.
Python не является потокобезопасным и изначально был разработан с так называемой GIL, или глобальной блокировкой интерпретатора, которая обеспечивает последовательное выполнение процессов на ЦП компьютера. На первый взгляд это означает, что программы Python не могут поддерживать многопроцессорность. Однако с тех пор, как был изобретен Python, были созданы процессоры (и графические процессоры), которые имеют несколько ядер. Современные параллельные программы используют преимущества этих нескольких ядер для одновременного запуска нескольких процессов:
- При многопоточности разные потоки используют разные процессоры, но каждый поток по-прежнему выполняется последовательно.
- При многопроцессорной обработке каждый процесс получает собственную память и вычислительную мощность, но процессы не могут взаимодействовать друг с другом.
Теперь вернемся к GIL. Используя преимущества современных многоядерных процессоров, программы, написанные на Python, могут также поддерживать многопроцессорность и многопоточность. Однако GIL по-прежнему гарантирует, что одновременно запускается только один поток Python. Итак, при программировании на Python:
- Используйте многопоточность, если вы знаете, что программа будет ожидать какого-либо внешнего события (например, для задач, связанных с вводом-выводом).
- Используйте многопроцессорность, когда ваш код может безопасно использовать несколько ядер и управлять памятью (например, для задач, связанных с процессором).
Установка Python
Если у вас уже установлен Python, этот шаг можно пропустить. Однако для тех, кто не читал, читайте дальше.
В этом руководстве я буду использовать Python от ActiveState, который создан на основе проверенного исходного кода и регулярно поддерживается для проверки безопасности. У вас есть два варианта:
- Загрузите и установите предварительно созданную среду выполнения Python Threading для Win10 или CentOS 7; или
- Если вы работаете в другой ОС, вы можете автоматически создать собственную пользовательскую среду выполнения Python, используя только те пакеты, которые вам потребуются для этого проекта, создав бесплатную учетную запись платформы ActiveState, после чего вы увидите следующее изображение:< /li>
ол>
- Нажмите кнопку "Начать" и выберите Python и ОС, в которой вам удобно работать. Выберите пакеты, которые вам понадобятся для этого руководства, включая rq.
- После сборки среды выполнения вы можете загрузить State Tool и использовать его для установки среды выполнения:
И все. Теперь вы установили Python в виртуальной среде.
Теперь мы можем перейти к самой интересной части — программированию!
Рекомендуемое чтение
Вернемся к нашему примеру загрузки 5 изображений из Интернета. Исходный код был:
Но теперь, когда мы знаем, что можем использовать потоки, давайте воспользуемся ими с функцией «загрузки»:
Выполнив этот код, вы обнаружите, что программа завершится намного раньше! Это связано с тем, что каждый раз, когда вызывается функция загрузки, она не выполняется в основном потоке процесса. Вместо этого создается новый поток, позволяющий выполнять каждую загрузку одновременно.
Следующая строка инициализирует поток, передает функцию для выполнения и ее аргументы.
Чтобы начать выполнение потока, все, что вам нужно сделать, это:
При выполнении кода вы заметите одну особенность: он завершается слишком быстро, и изображения загружаются не сразу. На самом деле изображения загружаются и записываются на диск спустя долгое время после завершения выполнения программы. Это связано с тем, что каждый поток продолжает обработку, даже если основной процесс завершил выполнение. Цель основного потока — только запустить поток, а не ждать его завершения.
Однако в некоторых сценариях вы захотите завершить основной процесс только после завершения выполнения всех дочерних потоков. В этом случае вы захотите использовать функцию `thread join` следующим образом:
При выполнении этого кода вы заметите, что программа работает немного дольше, но завершает выполнение только после загрузки всех изображений. Это связано с тем, что функция thread.join() ожидает, пока каждый поток «присоединится» к основному процессу.
Именно здесь важна общая память. Основной процесс знает о каждом созданном потоке и может дождаться завершения его обработки.
К счастью, с Python 3 теперь у вас есть доступ к ThreadPoolExecutor:
Вот оно! С помощью ThreadPoolExecutor вы можете инициализировать потоки из массива, запускать их все и ждать, пока все они снова присоединятся к основному процессу — и все это в одной строке кода!
Рекомендуемое чтение
Хотя кажется, что потоки Python идеально подходят для нашего варианта использования, когда вы на самом деле начнете реализовывать потоки в реальном сценарии, вы, вероятно, столкнетесь с множеством проблем. От условий гонки до взаимоблокировок — потоки могут оказаться весьма проблематичными, если не учитывать все проблемы, которые могут возникнуть при доступе к общим ресурсам.
Условия гонки
Когда два или более потока обращаются к одному и тому же общему ресурсу (например, к базе данных) одновременно, могут возникнуть странные ситуации. Когда оба потока одновременно пытаются обновить/изменить один и тот же объект в базе данных, окончательное значение объекта (т. е. какой поток выиграет) непредсказуемо. В результате необходимо принять дополнительные меры предосторожности, чтобы несколько потоков не обращались к общим ресурсам одновременно.
Взаимоблокировки
В других случаях, когда два или более потока запрашивают доступ к одному и тому же общему ресурсу, каждый поток может заблокировать другой. Это происходит, когда процессор пытается выяснить, какой поток получит доступ. Поскольку оба потока запросили доступ к ресурсу в одно и то же время, ресурс выглядит занятым для обоих потоков. В результате ресурс останется занятым навсегда, и ни один поток не получит доступа.
Другие проблемы
С потоками связано множество проблем, включая управление памятью, повторение процесса при сбое потока, точные отчеты о состоянии и многое другое. К сожалению, эти проблемы повторяются во многих реальных сценариях и быстро становятся пустой тратой ресурсов разработчиков, чтобы пытаться решить их снова и снова. К счастью, есть лучшая альтернатива. Давайте посмотрим!
Очереди — альтернатива потокам
Когда я работаю с API или любым другим процессом, требующим длительного времени обработки, я не смотрю на потоки. Вместо этого я смотрю на то, что называется очередями.
Очереди — это структуры данных "первым пришел — первым обслужен" (FIFO). Это простой способ выполнять задачи как синхронно, так и асинхронно, избегая условий гонки, взаимоблокировок и других проблем, обсуждавшихся ранее.
Во-первых, очереди «дешевле» с точки зрения затрат на обработку. Каждый раз, когда вы вызываете потоки, возникают накладные расходы на управление. Память необходимо назначать и отзывать после выполнения каждого потока. Во многих случаях осиротевшие потоки могут возникать, когда они не закрываются и не очищаются должным образом. Вместо этого потерянные потоки остаются висящими, отнимая драгоценное время ресурсов ЦП. С очередями затраты памяти для хранения выполнения очереди намного экономичнее.
Очереди также предоставляют простой способ мониторинга и повторного выполнения процессов. Я предлагаю прочитать «Потоки и очереди» Омара Эльгабри, чтобы глубже разобраться в этом.
Чтобы увидеть, как вы можете использовать очереди в нашем приложении, давайте воспользуемся rq (очередью Redis), очень популярной системой управления очередями в Python. Redis — это структура данных в памяти с открытым исходным кодом, которая обеспечивает быстрое чтение и запись. Думайте об этом как о невероятно быстрой базе данных, которая исчезает, когда вы ее отключаете.
При использовании rq каждое выполнение (или «задание») происходит путем сериализации идентификатора задания и всех необходимых параметров в Redis. Затем rq берет каждое задание из Redis, извлекает параметры и выполняет функцию. В случае сбоя выполнения задания у rq есть память, выделенная в Redis для того, какое задание не удалось выполнить, а также ресурсы, необходимые для его повторного запуска. В результате разработчики могут отслеживать процесс задания и перезапускать невыполненные задания.
Если вы не добавили RQ в свою среду выполнения, вы можете использовать pip:
Теперь давайте изменим наш код варианта использования, чтобы использовать преимущества очередей вместо потоков:
Прежде чем вы сможете выполнить этот код, вам нужно запустить рабочий процесс RQ. Рабочий процесс RQ — это фоновый механизм, который прослушивает новые задания, считывает данные из Redis и выполняет их.
Чтобы запустить сервер RQ:
Теперь вы можете просматривать журналы выполнения прямо здесь!
Дальнейшие шаги
Многопоточность, многопроцессорность и очереди могут стать отличным способом повышения производительности. Но прежде чем внедрять что-либо из этого, чрезвычайно важно понять ваши потребности и различия между различными механизмами фоновой обработки, прежде чем выбрать то, что подходит именно вам.
С большинством задач RQ справляется. Он широко используется во многих проектах с открытым исходным кодом и решает весь беспорядок, связанный с потоками!
Читайте также: