, Книга рецептов параллельного программирования Python. 2е изд

Книга рецептов параллельного программирования Python. 2е изд

Джанкарло Закконе

Copyright © 2019 Packt Publishing

Данный документ предоставляется по лицензии Creative Commons Attribution 3.0 License, за исключением разделов со специальными оговорками.

http://creativecommons.org/licenses/by/3.0/legalcode

Первая публикация на английском языке: Август 2015

Второе издание на английском языке: Сентябрь 2019

Ссылка на продукт: 1050919

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

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

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

Опубликовано Packt Publishing Ltd.

Livery Place

35 Livery Street

Birmingham B3 2PB, UK

ISBN 978-1-78953-373-6

www.packtpub.com

2019-10-01

 Состав исполнителей

Автор

Джанкарло Закконе

Редактор ввода в эксплуатацию

Рича Трайпатхи

Редактор приобретения

Чаитайнья Наир

Редактор разработки содержимого

Рувика Рао

Главный редактор

Афшаан Хан

Технический редактор

Гаурав Гайла

Литературный редактор

Safis Editing

Координатор проекта

Праджакта Найк

Корректор

Safis Editing

Составитель указателя

Рекха Наир

Технолог

Джошуа Мискюитта

 Об авторе

Джанкарло Закконе более пятнадцати лет управлял исследовательскими проектами в научной и промышленной областях. Он является программистом и системным инженером в Европейском космическом агентстве (ESTEC), где в основном связан с кибербезопасностью систем спутниковой навигации.

Джанкарло имеет степень магистра по физике и учёной степени магистра в области научных вычислений.

Джанкарло уже выступает приводимых далее книг, доступных в Packt: Python Parallel Programming Cookbook (Первое издание), Deep Learning with TensorFlow (Первое издание) и Deep Learning with TensorFlow (Второе издание).

 Рецензент

Др. Мишель Галлой является разработчиком программного обеспечения, сосредоточенным на высокопроизводительных вычислениях и визуализации в научном программировании. В целом он работает c IDL, но, случается, применяет Python, C и CUDA. Мишель в настоящее время работает в Национальном центре исследования атмосферы (NCAR) в Солнечной обсерватории Мауна Лоа. Ранее он работал в Tech-X Corporation, в которой он был главным разработчиком GPULib, библиотеки IDL привязок для процедур вычислений с GPU ускорением. Он создатель и основной разработчик IDLdoc, mgunit и rIDL, которые все являются проектами с открытым исходным кодом, а также автором Modern IDL.

Ричард Марсден имеет 25 летний опыт профессионального программирования. Начав с области геодезической съёмки для нефтяной отрасли, он потратил последние 15 лет на запуск Winwaed Software Technology LLC, независимого разработчика программного обеспечения. Winwaed специализируется на вебсайте Mapping-Tools для инструментов и дополнений под приложения геодезической съёмки, такие как Caliper Maptitude, Microsoft MapPoint, Android и Ultra Mileage.

Мишель выступал техническим рецензентом ряда изданий Packt, в том числе, Python Geospatial Development и Python Geospatial Analysis Essentials, обе принадлежащие Эрику Вестра; а также Python Geospatial Analysis Cookbook, Мишеля Дайинира.

 www.PacktPub.com

 Предисловие

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

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

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

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

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

И последнее, но не менее важное: параллельные вычисления представляют собой некую попытку максимизировать этот бесконечный, но в то же самое время, всё более ценный и дефицитный ресурс, ресурс времени.Именно по этой причине параллельные вычисления переходят из мира очень дорогостоящих суперкомпьютеров, резервированных для избранных, к более экономичным решениям, базирующимся на множестве процессоров, GPU (Graphics Processing Units , графическим процессорам), либо к нескольким взаимосвязанным компьютерам, которые способны преодолевать ограничения последовательных вычислений и пределов отдельных ЦПУ.

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

С технологической точки зрения, в Python отсутствует обособленный этап компиляции (как это, к примеру, происходит в C), который вырабатывает некий исполняемый файл из исходного кода. Тот факт, что он является псевдо интерпретатором, превращает Python в некий переносимый язык. После того как написан некий исходный код, он может быть интерпретирован и исполнен на большинстве применяемых в настоящее время платформ, будь то платформа от Apple (macOS X) или PC (Microsoft Windows и GNU/Linux).

Другой сильной стороной Python выступает его простота изучения. Всякий может научиться применять его через пару дней и написать своё первое приложение. В этом контексте, имеющаяся открытая структура данного языка играет фундаментальную роль, причём без каких бы то ни было дополнительных описаний, и тем самым чрезвычайно походит на некий разговорный язык. Наконец, Python является свободно распространяемым программным обеспечением: не только сам интерпретатор Python и само применение Python в наших приложениях доступны бесплатно, но также Python можно свободно изменять и тем самым распространять его в соответствии с установленными правилами лицензирования целиком открытого исходного кода.

Python Parallel Programming Cookbook, Second Edition, содержит широкое разнообразие примеров, которые предлагают читатель конкретную возможность решения реальных задач. Она изучает основные принципы проектирования программного обеспечения для параллельных архитектур, настаивая на несомненной важности ясности всех программ и избегая сложной терминологии в угодя чистоте и непосредственности примеров.

Каждая тема представлена как некая часть полной, причём рабочей программы на Python, за которой всегда следует вывод рассматриваемой программы. Модульная организация различных глав обеспечивает проторённую дорогу для перехода от самых простых аргументов к наиболее продвинутым,но также подходит и для тех, кто желает изучить только конкретные вопросы.

 Для кого эта книга

Python Parallel Programming Cookbook, Second Edition, предназначена для разработчиков программного обеспечения, которые желают применять технологии параллельного программирования для написания мощного и действенного кода. прочтение данной книги позволит вам набраться опыта как в имеющихся основах, так и в современных вопросах параллельного программирования.

Сам язык программирования Python прост для применения и позволяет не специалистам запросто схватить и уяснить те темы, которые выводятся данной книгой.

 Что охватывает эта книга

Глава 1. Приступая к параллельному программированию и Python представляет некий обзор архитектур параллельного программирования и моделей параллельного программирования. Эта глава служит введением в язык программирования Python, обсуждает как характеристики этого языка, его простота изучения и применения, его расширяемость и богатство доступных библиотек программного обеспечения, а также приложений, всё это делает Python ценным инструментом для любых приложений и в особенности, конечно, для параллельного программирования.

Глава 2. Параллельность на основе потоков обсуждает параллелизм потоков с применением модуля Python threading. Читатели на законченных примерах программ изучат как синхронизировать потоки и манипулировать ими для реализации в своих многопоточных приложениях.

Глава 3. Параллельность на основе процессов послужит для читателя руководством о основанному на процесcе подходу к параллельной программе. Полный набор примеров покажет читателю как применять модуль Python multiprocessing.

Глава 4. Обмен сообщениями сосредоточена на системах взаимодействия с обменом передаваемых сообщений. В частности, на множестве примеров приложений будет описана библиотека mpi4py.

Глава 5. Асинхронное программирование объясняет асинхронную модель одновременного программирования. До некоторой степени она проще нежели имеющиеся потоки, так как присутствует лишь отдельный поток инструкций, а задачи в явном виде освобождаются от управления вместо того чтобы подвисать произвольным образом. Данная глава показывает читателям как применять модуль asyncyio для организации всех задач в виде некой последовательности шагов меньшего размера, которые следует исполнять неким асинхронным образом.

Глава 6. Распределённый Python вводит читателя в распределённое программирование, который состоит в определённом процессе агрегации некоторых вычислительных единиц для совместного исполнения некой отдельной вычислительной задачи прозрачным и согласованным образом. В частности, представляемый в этой главе пример описывает использование модулей socket и Celery для управления распределёнными задачами.

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

Глава 8. Неоднородные вычисления рассмотрит современные GPU, которые предоставляют прорывную производительность для численных вычислений за счёт стоимости сложности программирования. В действительности, модели программирования GPU требуют чтобы сам программист вручную управлял бы необходимым обменом данных между ЦПУ и GPU. Эта глава обучит читателя на примерах программирования и вариантах применения тому,как эксплуатировать вычислительную мощность, предоставляемую картами GPU, используя мощные модули Python PyCUDA, Numba и PyOpenCL.

Глава 9. Отладка и тестирование Python является самой последней главой, которая вводит две важные темы инженерии программного обеспечения: отладку и тестирование. На практике будут пояснены следующие инфраструктуры: winpdb-reborn для отладки, а также unittest и nose для проверок программного обеспечения.

 Как получить от этой книги больше пользы

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

 Загрузка примеров кода

Вы можете загрузить файлы примеров кода по адресу из своей учётной записи: http://www.packtpub.com. Если вы приобрели книгу где- либо ещё, вы можете посетить http://www.packtpub.com/support и зарегистрироваться для получения этих файлов по электронной почте непосредственно на свой адрес.

Вы можете загрузить эти файлы с кодом выполнив следующие шаги:

Зарегистрируйтесь на нашем вебсайте при помощи своего адреса электронной почты и пароля.

Переместите указатель мыши на закладку SUPPORT в верхней части.

Кликните по Code Downloads & Errata.

Введите название книги в блок Search.

Выберите книгу для которой вы ищете для загрузки файлы исходного кода.

В ниспадающем меню выберите где вы приобрели эту книгу.

Кликните по Code Download.

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

WinRAR / 7-Zip для Windows

Zipeg / iZip / UnRarX для Mac

7-Zip / PeaZip для Linux

Весь код в единой упаковке также размещён на GitHub. В случае обновления этого кода, он также будет обновлён и в обозначенном репозитории GitHub.

У нас также имеются и прочие упаковки кодов из нашего богатого каталога книг и видео- материалов , доступными в GitHub. Ознакомьтесь с ним!

 Загрузка цветных изображений этой книги

Мы дополнительно снабжаем вас файлом PDF, который содержит цветные изображения экранных снимков/ схем, использованных в данной книге. Цветные изображения помогут вам лучше понять изменения в выводе. Вы можете загрузить этот файл по адресу: https://static.packt-cdn.com/downloads/9781789533736_ColorImages.pdf.

 Соглашения

В данной книге вы найдёте ряд текстовых стилей, которые делают разницу между различными видами информации. Здесь мы приводим некоторые примеры этих стилей и объяснение их назначения.

Кодовые слова в тексте, имена таблиц базы данных, имена папок, имена файлов, расширения файлов, имена путей, модели URL-адресов, ввод пользователя, и регулировки Twitter представлены следующим образом: "Исеется возможность убить некий процесс немедленно при помощи метода terminate".

Блок кода записываются следующим образом:

import socket
port=60000
s =socket.socket()
host=socket.gethostname()

Когда мы желаем привлечь ваше внимание к определённой части кода в блоке, соответствующие строки или элементы выделяются жирным шрифтом:

p = multiprocessing.Process(target=foo)
print ('Process before execution:', p, p.is_alive())
p.start()

Всякая командная строка ввода или вывода отображается так:

> python server.py

Жирным шрифтом отображаются новые термины или важные слова, которые вы видите на экране. Например, появляющиеся в тексте слова в меню или блоках диалогов. Вот некий пример: "Перейдите в System Properties | Environment Variables | User or System variables | New".

[Замечание]
Замечание

Предостережения или важные замечания появляются в блоках подобных этому.

[Совет]
Совет

Советы и ловкие приёмы возникают таким образом.

 Разделы

В этой книге вы будете находить отдельные часто повторяющиеся заголовки (Приготовление, Как это сделать..., Как это работает..., Также ознакомьтесь... и Дополнительно). Чтобы получить чёткие инструкции как выполнить определённый рецепт, применяйте эти разделы следующим образом:

Приготовление

Этот раздел сообщит вам что ожидает данный раздел и описывает как настроить всё необходимое для данного рецепта и выполнить предварительные настройки.

Как это сделать...

Данный раздел содержит все шаги, которые следует осуществить в нём.

Как это работает...

Этот раздел обычно размещает подробное объяснение того что происходило в предыдущем разделе.

Также ознакомьтесь...

В этом разделе содержится дополнительная информация о данном рецепте чтобы вы получили больше сведений о самом рецепте.

Дополнительно

Данный раздел предоставляет полезные ссылки на прочую полезную для обсуждаемого рецепта информацию.

 Обратная связь с читателями

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

Для отправки обычного отклика просто пошлите электронное письмо на адрес feedback@packtpub.com с упоминанием заголовка книги в теме вашего сообщения.

Если у вас существует тема, в которой у вас имеется опыт и вы заинтересованы либо в написании, либо во вкладе в книгу, обратитесь к руководству по адресу www.packtpub.com/authors.

 Поддержка пользователей

Теперь, когда вы являетесь гордым владельцем книги Packt, у нас есть целый ряд моментов для помощи вам в получении максимальной выгоды от вашей покупки.

 Опечатки

Хотя мы и предприняли все меры чтобы обеспечить точность нашего содержимого, ошибки всё- таки возможны. Если вы обнаружили ошибку в нашей книге - возможно, ошибку в тексте или в коде - мы будем признательны если вы сообщите об этом нам. Сделав это, вы можете предостеречь остальных читателей от разочарования и помочь нам улучшить последующие версии данной книги. Если вы обнаружили ошибку, пожалуйста, сообщите о ней посетив www.packtpub.com/submit-errata, выбрав вашу книгу, кликнув на ссылку Errata Submission Form, и заполнив подробности найденной вами ошибки. Когда ваша ошибка будет проверена, вы получите уведомление и ошибка будет выложена на наш веб- сайт или добавлена в какой- нибудь перечень существующих ошибок с заголовком раздела Errata.

Для просмотра ранее выявленных ошибок посетите www.packtpub.com/books/content/support и введите название нужной книги в поле поиска. Необходимая информация появится в разделе Errata.

 Незаконное тиражирование

Пиратство, защищённых авторским правом материалов в Интернете является постоянной проблемой во всех средствах массовой информации. В Packt мы подходим к защите наших авторских прав и лицензий очень серьёзно. Если вы столкнётесь с какой-либо незаконной копией наших работ в любой форме в Интернете, пожалуйста, предоставьте нам сразу адрес местонахождения или имя веб-сайта, чтобы мы могли принять меры. {Прим. пер.: Согласно закону об авторском праве РФ, авторские права на перевод принадлежат авторам этого перевода. Данным переводом, по нашему мнению, мы служим популяризации основных стратегических направлений развития тем Packt. В случае наличия конструктивных предложений, готовы к тесному сотрудничеству.}

Пожалуйста, обратитесь по адресу copyright@packtpub.com со ссылкой на материалы содержащие признаки нарушения авторских прав.

Мы выражаем вам признательность в защите наших авторов и нашей возможности доносить до вас имеющего ценность содержимого.

 Вопросы

Если у вас есть проблемы по любым сторонам данной книги, вы можете контактировать с нами по адресу questions@packtpub.com и мы предпримем все меры в отношении ваших проблем.

 Содержание

Предисловие

Что охватывает эта книга

Что вам нужно для этой книги

Для кого эта книга

Соглашения

Обратная связь с читателями

Поддержка пользователей

Загрузка кодов примеров

Опечатки

Незаконное тиражирование

Вопросы

Глава 1. Приступая к параллельному программированию и Python

Зачем нам требуется параллельное программирование?

Систематика Флинна

Один поток команд, один поток данных (SISD)

Много потоков команд, один поток данных (MISD)

Один поток команд, много потоков данных (SIMD)

Много потоков команд, много потоков данных (MIMD)

Организация памяти

Разделяемая память

Распределённая память

Массовая параллельная обработка (MPP)

Кластеры рабочих станций

Неоднородные архитектуры

Модели параллельного программирования

Модель с разделяемой памятью

Многопоточная модель

Модель передачи сообщений

Модель с параллельными данными

Разработка параллельной программы

Декомпозиция задачи

Назначение задачи

Агломерация

Установление соответствия

Динамическое соответствие

Оценка значения производительности параллельной программы

Ускорение

Эффективность

Масштабируемость

Закон Амдала

Закон Густафсона

Введение в Python

Функции подсказки

Синтаксис

Комментарии

Присвоения

Типы данных

Строки

Управление потоком

Функции

Классы

Исключительные ситуации

Импорт библиотек

Управление файлами

Охватывающий список

Запуск сценариев Python

Установка пакетов Python при помощи pip

Установка pip

Обновление pip

Применение pip

Введение в параллельное программирование Python

процессы и потоки

Глава 2. Параллельность на основе потоков

Что такое поток?

Модуль Python threading

Определение потока

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Выявление значения текущего потока

Приготовление

Как это сделать...

Как это работает...

Задание подкласса потока

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков блокировкой

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков RLock

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков при помощи семафоров

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков условием

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков событием

Приготовление

Как это сделать...

Как это работает...

Синхронизация потоков барьером

Приготовление

Как это сделать...

Как это работает...

Синхронизация потоков очередью

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Глава 3. Параллельность на основе процессов

Основы модуля множественности процессов Python

Порождение процесса

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Именование процесса

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Выполнение процессов в фоновом режиме

Приготовление

Как это сделать...

Как это работает...

Дополнительно

Уничтожение процессов

Приготовление

Как это сделать...

Как это работает...

Дополнительно

Определение процессов в неком подклассе

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Применение очереди для обмена данными

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Применение конвейеров для обмена объектами

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Синхронизация процессов

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Применение пула процессов

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Глава 4. Обмен сообщениями

Технические требования

Понимание структуры MPI

Применение модуля Python mpi4py

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Реализация взаимодействия точка- точка

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Обход проблем взаимной блокировки

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие через широковещание

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие при помощи функции разброса

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие при помощи функции получения

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие с применением Всех-ко-всем

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Операция свёртки

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Оптимизация взаимодействия

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Глава 5. Асинхронное программирование

Применение модуля Python concurrent.futures

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Управление основным циклом событий через asyncio

Понимание циклов событий

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Обработка сопрограммам при помощи asyncio

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Манипулирование задачами при помощи asyncio

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Работа с asyncio и фьючерсы

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Глава 6. Распределённый Python

Введение в распределённое программирование

Типы распределённых приложений

Клиент- серверные приложения

Клиент- серверная архитектура

Взаимодействие клиент- сервер

Архитектура TCP/IP клиент- сервер

Приложения со множеством уровней

Применение модуля Python socket

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Типы сокетов

Сокеты потоков

Дополнительно

Управление распределёнными задачами при помощи Celery

Приготовление

Установка Windows

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

RMI при помощи Pyro4

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Реализация топологии цепочки

Дополнительно

Глава 7. Облачные вычисления

Что такое облачные вычисления?

Основы архитектуры облачных вычислений

Модели служб

SaaS

PaaS

IaaS

Модели распределения

Общедоступное облако

Частное облако

Облачное сообщество

Гибридное облако

Платформы облачных вычислений

Разработка веб приложений при помощи PythonAnywhere

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Докеризация приложения Python

Приготовление

Установка Docker for Windows

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Введение в вычисления без серверов

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Что такое функция Лямбда?

Почему без сервера?

Возможные проблемы и ограничения

Дополнительно

Глава 8. Неоднородные вычисления

Основы неоднородных вычислений

Основы архитектуры GPU

Основы программирования GPU

CUDA

OpenCL

Работа с PyCUDA

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Гетерогенное программирование с применением PyCUDA

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Реализация управления памятью с применением PyCUDA

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Введение в PyOpenCL

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Построение приложений при помощи PyOpenCL

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Поэлементные выражения с применением PyOpenCL

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Оценка приложений PyOpenCL

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

За OpenCL и PyOpenCL

Против OpenCL и PyOpenCL

За CUDA и PyCUDA

Против CUDA и PyCUDA

Дополнительно

Программирование GPU с применением Numba

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Глава 9. Отладка и тестирование Python

Что представляет собой отладка?

Что такое тестирование программного обеспечения?

Отладка при помощи Winpdb Reborn

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Взаимодействие через pdb

Приготовление

Взаимодействие через командную строку

Применение интерпретатора Python

Вставка директив в сам код для отладки

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Реализация rpdb для отладки

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Работа с unittest

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Тестирование приложений при помощи nose

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Указатель

Глава 1. Приступая к параллельному программированию и Python

Содержание

Глава 1. Приступая к параллельному программированию и Python

Зачем нам требуется параллельное программирование?

Систематика Флинна

Один поток команд, один поток данных (SISD)

Много потоков команд, один поток данных (MISD)

Один поток команд, много потоков данных (SIMD)

Много потоков команд, много потоков данных (MIMD)

Организация памяти

Разделяемая память

Распределённая память

Массовая параллельная обработка (MPP)

Кластеры рабочих станций

неоднородные архитектуры

Модели параллельного программирования

Модель с разделяемой памятью

Многопоточная модель

Модель передачи сообщений

Модель с параллельными данными

Разработка параллельной программы

Декомпозиция задачи

Назначение задачи

Агломерация

Установление соответствия

Динамическое соответствие

Оценка значения производительности параллельной программы

Ускорение

Эффективность

Масштабируемость

Закон Амдала

Закон Густафсона

Введение в Python

Функции подсказки

Синтаксис

Комментарии

Присвоения

Типы данных

Строки

Управление потоком

Функции

Классы

Исключительные ситуации

Импорт библиотек

Управление файлами

Охватывающий список

Запуск сценариев Python

Установка пакетов Python при помощи pip

Установка pip

Обновление pip

Применеие pip

Введение в параллельное программирование Python

Процессы и потоки

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

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

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

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

В этой главе мы рассматриваем следующие темы:

К чему вам параллельное прграммирвоание?

Систематика Флинна

Организация памяти

Модели параллельного программирования

Оценка производительности

Введение в Python

Python и параллельное прграммирование

Введенеи в процессы и потоки

Зачем нам требуется параллельное программирование?

Происходящий рост вычислительной мощности ставший доступным в современных компьютерах в результате привёл нас к столкновению с вычислительными проблемами возрастающей сложности в отношении коротких временных кадров. Вплоть до ранних 2000-х, сложность имела отношение к возрастающему числу транзисторов, а также временной частоте систем с единственным процессором, которые достигли пика в 3.5- 4ГГц. Однако рост числа транзисторов вызвал экспоненциальное увеличение рассеиваемой мощности на самих процессорах. На практике, следовательно, существует некое физическое ограничение, которое препятствует дальнейшему улучшению систем с единственным процессором.

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

С другой стороны, столь значительное изменение в оборудовании также имело результатом и некую эволюцию структуры программного обеспечения, которое всегда разрабатывалось для последовательного исполнения в неком отдельном процессоре. Для получения преимуществ имеющихся больших вычислительных ресурсов предоставляемого доступным ростом числа процессоров, требуется по- новому проектировать имеющееся программное обеспечение в неком виде подхода к имеющейся параллельной структуре установленных ЦПУ с тем, чтобы получать более высокую эффективность посредством одновременного исполнения во всех отдельных устройствах частей одной и той же программы.

Систематика Флинна

Систематика Флинна выступает в качестве некой системы для классификации архитектур компьютеров. Она основывается на двух главных понятиях:

Потоке инструкций: некая система с n ЦПУ имеет n программных счётчиков и, таким образом, n потоков инструкций. Это относится к некому программному счётчику.

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

Так как имеющиеся инструкции и потоки данных независимы, существует четыре категории параллельных машин: Один поток команд, один поток данных (SISD - Single Instruction Single Data), Много потоков команд, один поток данных (MISD - Single Instruction Multiple Data), Один поток команд, много потоков данных (SIMD - Multiple Instruction Single Data), Много потоков команд, много потоков данных (MIMD - Multiple Instruction Multiple Data):

Рисунок 1-1

Систематика Флинна

Один поток команд, один поток данных (SISD)

Вычислительная система SISD подобна машине фон Неймана, которая является машиной с единственным процессором. Как вы могли видеть на схеме систематики Флинна, она исполняет отдельную команду (инструкцию), которая обрабатывает отдельный поток (strem) данных. В SISD машинные команды обрабатываются последовательно.

Во временном цикле её ЦПУ исполняет следующие операции:

Fetch (выборка): Данный ЦПУ выполняет выборку данных и инструкций из некой области памяти, которая именуется регистром.

Decode (декодирование): Рассматриваемый нами ЦПУ декодирует полученную команду (инструкцию).

Execute (исполнение): Декодированная команда выполняется для полученных данных. Получаемый результат данной операции сохраняется в другом регистре.

После завершения исполнения этапа исполнения, данный ЦПУ устанавливает себя в начало другого цикла ЦПУ:

Рисунок 1-2

Цикл выборки, декодирования и выполнения

Те алгоритмы, которые выполняются в этом типе компьютера являются последовательными (или серийными), так как они не содержат никакого параллелизма. Неким образцом компьютера SISD выступает система оборудования с единственным ЦПУ.

Основными элементами таких архитектур (а именно, архитектур фон Неймана) являются такие:

Центральное устройство памяти: Оно применяется для хранения как инструкций, так и данных программы.

ЦПУ: Используется для получения инструкций и/ или данных из своего устройства памяти, причём декодирует получаемые команды и последовательно реализует их.

Система ввода/ вывода: Обозначает собственно ввод и выод данных для данной программы.

Обычные однопроцессорные компьютеры классифицируются как системы SSD:

Рисунок 1-3

Схема архитектуры SISD

Наша следующая схема в частности отображает какие области применяются на стадиях выборки, декодирования и выполнения:

Рисунок 1-4

Компоненты ЦПУ на этапе выборки, декодирования и выполнения

Много потоков команд, один поток данных (MISD)

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

В этом случае присутствующий параллелизм (параллелизм уровня инструкций) достигается за счёт выполнения нескольких операций с одними и теми же данными. Те типы задач, которые могут действенно решаться в подобных архитектурах достаточно специфичны, к примеру, шифрование данных. По этой причине подобные компьютеры MISD невозможно отыскать в имеющемся коммерческом секторе. Компьютеры больше относятся к умственному примеру, нежели к какой- то практической конфигурации.

Один поток команд, много потоков данных (SIMD)

Компьютер SIMD состоит из n идентичных процессоров, каждый из которых обладает своей собственной локальной памятью, в которой он способен хранить данные. Все процессоры работают под управлением единственного потока (stream) команд. Помимо этого, имеются n потоков данных, по одному для каждого процессора. Эти процессоры работают одновременно на каждом шаге и выполняют одни и те же инструкции, но для различных элементов данных. Это некий пример параллелизма данных.

Подобные архитектуры SIMD являются более многосторонними, нежели архитектуры MISD. Параллельные алгоритмы на компьютерах SIMD способны решать множество задач, охватывающих широкий диапазон приложений. Другим интересным свойством является то, что подобные алгоритмы для таких компьютеров относительно просты для проектирования, анализа и реализации. Единственным ограничением является то, что компьютерами SIMD могут решаться только те задачи, которые могут делиться на некое число подзадач (которые все идентичны, причём каждая из них будет решаться одновременно тем же самым набором инструкций).

В отношении суперкомпьютеров, разработанных в соответствии с этой парадигмой, нам следует упомянуть Connection Machine (Thinking Machine, 1985) и MPP (NASA, 1983).

Как мы обнаружим в Главе 6, Распределённый Python и Главе 7, Облачные вычисления, появление современных видеокарт (GPU), построенных на множестве встроенных SIMD устройств, привело к более широкому применению данной вычислительной парадигмы.

Много потоков команд, много потоков данных (MIMD)

Этот класс параллельных компьютеров является наиболее распространённым и наиболее мощным классом, в соответствии с классификацией Флинна. Он содержит n процессоров, n потоков инструкций и n потоков данных. Каждый процессор имеет своё собственное управляющее устройство и локальную память, что превращает архитектуры MIMD в более мощные вычислительно, нежели архитектуры SIMD.

Каждый процессор работает под управлением некого потока инструкций, вызываемых его собственным устройством управления. Следовательно, такие процессоры способны потенциально выполнять различные программы с различными данными, что позволяет им решать подзадачи, которые являются различными и могут быть частью одной задачи большего размера. В MIMD данная архитектура достигается при помощи такого уровня параллелизма за счёт потоков (threads) и/ или процессов. Это также означает, что данные процессоры обычно работают асинхронно.

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

Рисунок 1-5

Архитектура SIMD (a) и архитектура MIMD (B)

Систематику Флинна можно расширить с учётом того, что машины SIMD можно подразделить на две подгруппы:

Вычислительные суперкомпьютеры

Векторные машины

С другой стороны, MIMD можно подразделять на машины обладающие совместно используемой памятью и те, которые имеют распределённую память.

И действительно, наш следующий раздел посвящён этому последнему аспекту организации самой памяти машин MIMD.

Организация памяти

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

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

Когда некий процессор инициирует какой- то обмен в или из памяти, имеющиеся ресурсы процессора будут оставаться занятыми на протяжении всего цикла памяти; следовательно, в этот период времени никакие устройства (например, контроллер ввода/ вывода, процессор или даже тот процессор, который сделал данный запрос) не будут в состоянии применять эту память из- за выполнения данного обмена:

Рисунок 1-6

организация памяти для MIMD архитектуры

Решением данной проблемы доступа к памяти привели в результате к разветвлению архитектур MIMD. Самый первый тип систем, называемый системой с разделяемой памятью, обладает роскошной виртуальной памятью, а все процессоры имеют эквивалентный доступ к данным и инструкциям из этой памяти. Другим типом системы является модель с распределённой памятью, в которой каждый процессор имеет локальную память, которая не доступна прочим процессорам.

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

В частности, некая машина с распределённой памятью обязана делать копии совместной памяти в каждой локальной памяти. Такие копии создаются отправкой сообщений, содержащих те данные, которые должны разделяться (совместно использоваться) между одним и другим процессорами. Чёрной стороной такой организации памяти является то, что порой такие сообщения могут быть очень большими и требовать относительно продолжительного времени для передачи, в то время как в некой системе с разделяемой памятью обмен сообщениями отсутствует, а основная проблема состоит в синхронизации доступа к совместно используемым ресурсам.

Разделяемая память

На приводимой ниже диаграмме показана схема совместно используемой памяти. Все физические связи здесь достаточно просты:

Рисунок 1-7

Схема архитектуры с разделяемой памятью

В данном случае имеющаяся структура шины позволяет некому произвольному числу устройств (ЦПУ + Кэш на предыдущей схеме), которые разделяют один и тот же канал (Главная память, как это отображено на схеме выше). Такие протоколы шины изначально проектировались чтобы позволить некому отдельному процессору и одному или более дискам или ленточным контроллерам взаимодействовать здесь через совместно используемую память.

[Совет]
Совет

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

Основная проблема возникает когда некий процессор изменяет данные, хранящиеся в той памяти системы, которая одновременно используется другими процессорами. Такое новое значение будет передано из кэша того процессора, который произвёл изменения, в общую разделяемую память. Позднее, однако, оно должно быть передано во все прочие процессоры с тем, чтобы они не работали с устаревшими данными. Эта проблема именуется задачей когерентности кэша - некий особый случай согласованности памяти, которая требует аппаратной реализации, способной обрабатывать проблемы одновременности и синхронизации и аналогичны программированию потоков (thread).

Основными свойствами систем с разделяемой памятью являются следующие:

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

необходимая синхронизация достигается за счёт чтения самих задач различных процессоров и предоставления разделяемой памяти. На самом деле эти процессоры за раз могут получить доступ только к одной области памяти.

Некая совместно используемая память не может изменяться из какой- то задачи пока другая задача имеет к ней доступ.

Разделение данных между задачами является быстрым. То время, которое требуется для взаимодействия состоит в значении времени, которое требуется одной из задач для считавания отдельного местоположения (и зависит от самой скорости доступа к памяти).

Сам доступ к совместно используемой памяти в системах бывает следующим:

UMA (Uniform Memory Access, Единообразный доступ к памяти): Основной характеристикой таких систем является то, что время доступа к имеющейся памяти является постоянным значением для всех процессоров и для любых областей памяти. По этой причине такие системы также называются SMP (Symmetric Multiprocessors). Они достаточно просты в реализации, однако не очень масштабируемы. Сам составитель кода отвечает за управление синхронизацией, вставляя надлежащий контроль, семафоры, блокировки и прочее в свои программы, которые управляют ресурсами.

NUMA (Non-Uniform Memory Access, Неоднородный доступ к памяти): Эти архитектуры подразделяют имеющуюся память на области с выской скоростью доступа, которые выделяются каждому процессору, а также некой общей области собственно для обмена данными, причём с более медленным доступом. Такие системы также именуются системами DSM ( Distributed Shared Memory, с распределённой общей памятью). Они чрезвычайно масштабируемы, но сложны в разработке.

[Замечание]
Замечание

{Прим. пер.: Следует отметить, что неоднородность доступа к памяти также зависит и от уровня программного обеспечения. К примеру, современные ЦПУ имеют встроенный контроллер памяти и в то же время обладают общим адресным пространством. Это, например, позволяет на уровне операционной системы выделять одному и тому же процессу и/ или потоку области памяти, управляемые физически различными ЦПУ. В результате, с точки зрения приложения выделенная ему память будет иметь тип UMA, хотя при этом могут иметься различия во временах доступа к различным адресам в общем адресном пространстве. Эти различия могут быть ещё более существенными для приложений, работающих в архитектурах NUMA, имеющих более глубокие связи интерконнекта и располагающиеся в физически обособленных системах.}

NoRMA (No Remote Memory Access, доступ к памяти без удалённого доступа): Всякая локальная память предоставляется для частного использования и может иметь доступ только с локального процессора. Необходимое взаимодействие между процессорами состоит в неком протоколе взаимодействия, применяемом для обмена сообщениями, который именуется протоколом передачи сообщений (message-passing protocol).

COMA (Cache-Only Memory Architecture, архитектура исключительно с кэшируемой памятью): Такие архитектуры снабжаются исключительно памятью кэширования. При анализе архитектур NUMA было отмечено, что такая архитектура удерживает локальные копии данных в кэше и что эти данные сохранялись в виде дубликатов в своей главной памяти. Данная же архитектура избавляется от дублирования и оставляет лишь свои кэш области памяти; вся память физически распределяется между имеющимися процессорами (локальная память). Все локальные области памяти являются частными и к ним могут выполнять доступ лишь их локальные процессоры. Необходимое взаимодействие между процессорами также происходит через протокол передачи сообщений.

Распределённая память

В некой системе с распределённой памятью сама память ассоциирована с каждым процессором и некий процессор имеет возможность адресации только к своей собственной памяти. Некоторые авторы называют данный вид систем мультикомпьютером, отражая тот факт, что все элементы такой системы, фактически, являются маленькими и законченными системами из неких процессора и памяти, как вы это можете наблюдать на следующей схеме:

Рисунок 1-8

Схема архитектуры с распределённой памятью

Такой вид организации обладает рядом преимуществ:

Нет никаких конфликтов на самом уровне имеющихся шины или коммутатора взаимодействия. Всякий процессор может применять полную полосу пропускания своей собственной локальной памяти без каких либо помех со стороны прочих процессоров.

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

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

Основным недостатком является то, что взаимодействие между процессорами более сложное для реализации. Когда процессору требуются данные из памяти другого процессора, тогда таким двум процессорам обязательно нужно обмениваться сообщениями через протокол передачи сообщений. Это приводит к двум источникам замедления: для построения и отправки сообщений от одного процессора к другому требуется время, и к тому же все процессоры должны останавливаться для управления получением сообщений от прочих процессоров. Некая программа, спроектированная для работы в какой- то машине с распределённой памятью должна быть организована в виде набора независимых задач, которые взаимодействуют через сообщения:

Рисунок 1-9

Основы обмена сообщениями

Основные свойства систем с распределённой памятью следующие:

Память физически распределена между процессорами; всякая локальная память напрямую доступна только из своего процессора.

Синхронизация достигается перемещением данных (даже если это всего лишь само по себе сообщение) между процессорами (взаимодействие).

Само подразделение данных в имеющихся локальных областях памяти воздействует на производительность такой машины - существенно выполнять деление аккуратно, с тем, чтобы свести к минимуму взаимодействие между имеющимися ЦПУ. Дополнительно к этому, тот процессор, который осуществляет координацию таких операций по разбиению и составлению должен эффективно взаимодействовать со всеми процессорами, которые работают как самостоятельные части структур данных.

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

Массовая параллельная обработка (MPP)

Машины MPP составляются из сотен процессоров (и может достигать сотен и тысяч процессоров в некоторых машинах), которые соединяются некой сетевой средой взаимодействия. На таких архитектурах основаны самые быстрые в мире компьютеры; некими представителями таких архитектур являются Earth Simulator, Blue Gene, ASCI White, ASCI Red, а также ASCI Purple и Red Storm.

Кластеры рабочих станций

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

В некой кластерной архитектуре мы определяем некий узел (node) как отдельный вычислительный элемент, который выступает частью в общем кластере. Для самого пользователя подобный кластер полностью прозрачен - все имеющиеся сложности оборудования и программного обеспечения маскируются, а данные и приложения делаются доступными, как если бы все они были доступны из некого отдельного узла.

Здесь мы выделим три вида кластеров:

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

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

Кластер высокопроизводительных вычислений: В нём все узлы настроены на воспроизводство чрезвычайно высокой производительности. Сам процесс также делится на множество заданий во множестве узлов. Эти задания распараллеливаются и будут распределяться по различным машинам.

Неоднородные архитектуры

Введение ускорителей GPU в имеющийся однородный мир суперкомпьютеров изменило саму природу того, как суперкомпьютеры и применяются, и программируются в настоящее время. несмотря на предлагаемую GPU высокую производительность, их нельзя рассматривать в качестве автономных процессоров, так как они всегда должны сопровождаться сочетанием с ЦПУ. Тем самым сама парадигма программирования очень проста: имеющийся ЦПУ берёт на себя управление и последовательно выполняемые вычисления, назначая те задачи, которые очень затратны в вычислительном отношении и обладают высокой степенью параллелизма, своему графическому ускорителю.

Необходимое взаимодействие между ЦПУ и GPU может происходить не только посредством использования высокоскоростной шины, но также и путём совместного использования отдельной области памяти как для физической, так и для виртуальной памяти. Фактически, в том случае, когда оба устройства не оснащены собственными областями памяти, можно ссылаться на общую область памяти применяя имеющиеся библиотеки программного обеспечения, предоставляемые различными моделями программирования, такими как CUDA и OpenCL.

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

Таким образом, несмотря на то, что ЦПУ и GPU не кажутся работающими действенно совместно, благодаря применению такой новой архитектуры мы можем оптимизировать их взаимодействие, также как и показатели производительности, для параллельных приложений:

Рисунок 1-10

Схема гетерогенной архитектуры

В своём следующем разделе мы сделаем введение в основные модели параллельного программирования.

Модели параллельного программирования

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

Говоря точно, ни одна из моделей не имеет абсолютных преимуществ перед прочими. По этой причине, самое лучшее решение, котрое будет использовано, будет во многом зависеть от той задачи, которую обязан рассмотреть и решить программист. Наиболее распространённые модели для параллельного программирования таковы:

Модель с совместно используемой памятью

Модель со множеством потоков

Модель с распределённой памятью/ передачей сообщений

Модель параллельных данных

В этом рецепте мы дадим вам некий обзор таких моделей.

Модель с разделяемой памятью

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

Многопоточная модель

В этой модели некий процесс может иметь множество потоков (flows) вычислений. Например, создаётся какая- то последовательная часть, а вслед за этим, создаётся некая последовательность задач, которые могут исполняться параллельно. Обычно такой тип модели используется в архитектурах с разделяемой памятью. Поэтому для нас будет очень важно управлять синхронизацией между потоками (threads), так как они работают с общей памятью и самому программисту требуется предотвращать обновления множеством потоков одной и той же области в одно и то же самое время {Прим. пер.: см. Ресторан Серийных ботов Цалеба Хаттингха}.

ЦПУ текущего поколения многопоточны как в отношении программного обеспечения, так и с аппаратной точки зрения. Потоки POSIX (сокращение для Portable Operating System Interface, интерфейса переносимой операционной системы) являются классическими примерами такой реализации многопоточности в программном обеспечении. Технология Hyper-Threading Intel реализует многопоточность на аппаратном уровне путём переключения между двумя потоками (threads), когда один из них приостанавливается или ожидает ввода/ вывода. За счёт этой модели можно достигать параллелизма даже когда расположение данных нелинейно.

Модель передачи сообщений

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

Некоторые образцы имелись начиная с 1980-х, но только в середине 1990-х была создана некая стандартизованная модель, которая привела к фактическому стандарту с названием MPI (Message Passing Interface, Интревейс передачи сообщений).

Данная модель MPI очевидно разрабатывалась для распределённой памяти, однако, будучи моделью параллельного программирования, некая модель со множеством платформ также может применяться и в машинах с разделяемой памятью:

Рисунок 1-11

Модель парадигмы передачи сообщений

Модель с параллельными данными

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

Для реализации данной модели пишущий код должен разработать некую программу, которая определяет распределение и группировку данных; например, GPU современного поколения обладают высокой производительностью только когда данные (Task 1, Task 2 и Task 3) сгруппированы так, как это показано на следующей схеме:

Рисунок 1-12

Модель парадигмы параллельных данных

Разработка параллельной программы

разработка эксплуатирующих параллелизм алгоритмов основывается на некой последовательности действий, которые следует выполнять для данной программы чтобы правильно осуществлять само задание бкз воспроизводства частичных или ошибочных результатов. Для верного распараллеливания алгоритма следует выполнять такие макро- операции:

Декомпозиция задачи

Назначение задачи

Агломерация

Установление соответствия

Декомпозиция задачи

На этом этапе сама создаваемая программа расщепляется на задачи или множество инструкций, которые могут исполняться в различных процессорах для реализации параллельности. Для осуществления такого деления применяются два метода:

Декомпозиция по областям: В этом случае делятся сами данные этих задач. Собственно приложение является общим для всех тех процессоров, которые работают с различными порциями данных. Эта методология применяется когда у нас имеются большие объёмы данных, подлежащие обработке.

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

Назначение задачи

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

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

Агломерация

Агломерация является процессом сочетания задач меньшего размера в более крупные для улучшения производительности. Если наши два предыдущих этапа процесса проектирования разбили общую проблему на некое число задач, которые намного превосходят общее число доступных процессоров и когда наш компьютер не разработан специально для обработки гигантского числа маленьких задач (некоторые архитектуры, например, GPU, справляются с этим, и на самом деле выигрывают от выполнения миллионов или даже миллиардов задач), тогда наш проект может стать очень неэффективным.

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

Установление соответствия

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

Часто взаимодействующие между собой задачи следует помещать в одном и том же процессоре для увеличения локализации.

Все задачи, которые могут исполняться одновременно, следует помещать а различные процессоры для увеличения одновременности.

Это имеет название проблемы соответствия, причём она известна как NP-полная задача. А раз так, в общем случае не может существовать никакого решения с полиномиальным временем для этой проблемы. Для задач с одним размером и задач с просто выявляемыми шаблонами взаимодействия, такое соответствие является простым (здесь мы также можем выполнять агломерацию для объединения задач, отображаемых в один и тот же процессор). Однако, когда задачи обладают шаблонами взаимодействия, которые трудно предсказуемы, либо объёмы работы разнятся от задачи к задаче, тогда сложно разработать действенную схему установления соответствия и агломерации.

Для такого типа задач можно применять алгоритмы балансировки нагрузки чтобы выявлять стратегии агломерации и установления соответствия в процессе времени исполнения. Самыми сложными задачами являются те, в которых объём взаимодействия или общее число задач изменяется в процессе исполнения данной программы. Для такого вида задач можно применять алгоритмы динамичной балансировки нагрузки, которые периодически запускаются на протяжении времени исполнения.

Динамическое соответствие

Для некого разнообразия задач имеется ряд алгоритмов балансировки нагрузки:

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

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

Однако сокращение накладных расходов способно сократить общее время исполнения, даже когда установление соответствия само по себе хуже. Когда задачи редко обмениваются данными, помимо обмена в самом начале и по завершению выполнения, тогда часто применяется некий алгоритм планирования задач, который просто устанавливает соответствие тем процессорам, которые простаивают. В неком алгоритме планирования задач поддерживается какой- то пул задач. Задачи помещаются в этот пул и изымаются обработчиками (workers).

При такой модели имеются три распространённых подхода:

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

Иерархический диспетчер/ обработчик: Это определённый вариант диспетчер/ обработчика, который имеет частично распределённую схему. Обработчики разделяются на группы, причём каждая со своим собственным диспетчером. Такие диспетчеры групп взаимодействуют со своим центральным диспетчером (и возможно также и промеж собой), в то время как обработчики запрашивают задачи от своего диспетчера группы. Это расделяет общую нагрузку по различным диспетчерам и способно, тем самым, обрабатывать большее число процессоров чем если бы обработчики запрашивали задачи у одного и того же самого диспетчера.

Децентрализация: В этой схеме всё децентрализовано. Каждый процессор сопровождает свой собственный пул задач и взаимодействует с прочими процессорами чтобы запрашивать задачи. То, как эти процессоры выбирают прочие процессоры для запроса задач варьируется и определяется на основе самой общей задачи.

Оценка значения производительности параллельной программы

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

Значение производительности достигается путём анализа и количественной оценки применяемых общего числа потоков и/ или общего числа процессов. Для такого анализа давайте введём несколько индексов производительности:

Ускорение

Эффективность

Масштабируемость

Показатели ограничений параллельных вычислений вводятся законом Амдала. Для оценки степени эффективности величины распараллеливания некого последовательного алгоритма у нас имеется закон Густафсона.

Ускорение

Величина ускорения это та мера, которая отображает значение преимущества параллельного решения некой задачи. Она определяется как значение соотношения времени, требуемого на решение задачи отдельным обрабатывающим элементом (Ts) к величине времени на решение той же самой задачи p идентичными обрабатывающими элементами (Tp).

Мы выражаем ускорение следующим образом:

					S = Ts / Tp

Когда у нас имеется линейная зависимость ускорения, S=p, это означает, что значение скорости выполнения растёт вместе с числом процессоров. Конечно, это некий идеальный случай. В то время как значение ускорения будет абсолютным когда Ts равняется времени исполнения самого наилучшего последовательного алгоритма, величина ускорения является относительной когда Ts равняется времени исполнения рассматриваемого параллельного алгоритма для некого отдельного процессора.

Давайте переобуем эти условия:

S = p является линейным, или идеальным ускорением.

S < p является реальным ускорением.

S > p является превышающим линейное, супер ускорением.

Эффективность

В неком идеальном мире, какая- то параллельная система с p обрабатывающими элементами может предоставить нам ускорение, которое равняется p. Тем не менее, это крайне редко достигается. Как правило, какое- то время тратится на простои или взаимодействие. Эффективность является мерой того, сколько времени исполнения некого обрабатывающего элемента идёт на выполнение полезной работы, а какая часть времени теряется.

Мы обозначим её как E и определим следующим образом:

					E = S / p = Ts / (pTp)

Те алгоритмы, у которых ускорение линейное, будут иметь значение E = 1. Во всех прочих случаях они будут иметь значение E меньшим чем 1. Три существенных варианта указывают нам следующее:

Когда E = 1, это случай линейного ускорения.

Когда E < 1, это некий реальный случай.

Когда E << 1, это некая задача, которая распараллеливается крайне неэффективно.

Масштабируемость

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

В зависимости от различных факторов, конкретная масштабируемая система при её росте может сохранять ту же самую эффективность или улучшать её.

Закон Амдала

Закон Амдала является широко применяемым законом, который используется при проектировании процессоров и параллельных алгоритмов. Он постулирует, что максимальное ускорение, которого можно достигнуть, ограничивается величиной последовательной составляющей в рассматриваемой программе:

					S  = 1 / (1 - p)

[Совет]
Совет

1 - p показывает величину последовательной составляющей (не подлежащей распараллеливанию) в некой программе.

Это означает, к примеру, что когда некая программа, в которой 90% кода может выполняться параллельно, а 10% должно оставаться последовательным, тогда максимальным значением ускорения является 9, причём даже при неком бесконечном числе процессоров.

[Замечание]
Замечание

{Прим. пер.: Ещё одним известным следствием Закона Амдала является тот факт, что максимальное ускорение системы из некого числа обрабатывающих элементов определяется производительностью самого медленного из них.}.

Закон Густафсона

Закон Густафсона постулирует следующее:

					S(P)  = P - α(1 - p)

Здесь мы определяем указанные в уравнении величины следующим образом:

P это значение числа процессоров

S это величина фактора ускорения

α это не поддающаяся распараллеливанию часть любого параллельного процесса.

Закон Густафсона отличен от закона Амдала, который, как мы уже описывали, предполагает, что общая рабочая нагрузка не изменяется по отношению к имеющемуся числу процессоров.

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

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

Введение в Python

Python это мощный, динамичный и интерпретируемый язык программирования, который применяется в широком разнообразии приложений. Вот некоторые из его свойств:

Ясниый и доступный для чтения синтаксис

Очень всесторонняя стандартная библиотека в еоторой, помимо дополнительных программных модулей мы способны добавлять типы данных, функции и объекты.

Простые для обучения быстрые разработка и отладка. Разработка Python кода на Python может быть до 10 раз быстрее чем в коде C/ C++. Этот код может работать в качестве прототипа и затем преобразовываться в C/ C++.

Обработка ошибок на основе исключительных ситуаций.

Функциональность строгого самоанализа.

Богатство документации и сообщества программирования.

Python можно рассматривать в качестве некого склеивающего языка. При помощи Python можно разрабатывать лучшие приложения, так как различные виды разработчиков кода могут совместно работать над неким проектом. Например, при построении некого научного приложения, программисты C/ C++ могут реализовывать эффективные численные алгоритмы, в то время как научные сотрудники в том же самом проекте могут писать программы Python, которые проверяют и применяют эти алгоритмы. Научным сотрудникам нет нужды изучать язык программирования нижнего уровня, а программистам C/ C++ не требуется понимать то, во что вовлечены эти научные сотрудники.

[Замечание]
Замечание

Вы можете прочесть об этом дополнительно в https://www.python.org/doc/essays/omg-darpa-mcc-position.

Давайте рассмотрим некоторые образцы очень базового кода для получения некой основы свойтв Python.

[Замечание]
Замечание

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

Функции подсказки

Сам интерпретатор Python уже предоставляет некую действенную систему подсказок. Если вы пожелаете узнать как применять некий объект, тогда просто наберите help(object).

Давайте посмотрим, например, как применить функцию help для целого 0:

>>> help(0)
Подсказка по объекту int:

class int(object)
 | int(x=0) -> integer
 | int(x, base=10) -> integer
 |
 | Преобразовывает некое число или строку в целое, либо возвращает 0 когда
 | не задан никакой аргумент. Когда x является неким числом, djpdhfoftn x.__int__(). Для
 | числ с плавающей запятой, также выполняется усечение в сторону нуля.
 |
 | Когда x не является неким числом или когда определено основание, тогда x должно быть некой строкой,
 | байтами, или экземпляр массива байт, представленный как некая целая константа по заданному
 | основанию. Перед такой константой может идти '+' или '-', а также она может выравниваться
 | пробельными символами. Основанием по умолчанию выступает 10. Допустимыми основаниями являются 0
 | и 2-36.
 | Основание 0 подразумевает интерпретацию значения основания из значения строки в качестве целой
 | константы.
>>> int('0b100', base=0)

За данным описанием объекта int следует перечень методов, которые применимы для него. Самыми первыми методами являются такие:

| Methods defined here:
 |
 | __abs__(self, /)
 | abs(self)
 |
 | __add__(self, value, /)
 | Возвращает self+value.
 |
 | __and__(self, value, /)
 | Возвращает self&value.
 |
 | __bool__(self, /)
 | self != 0
 |
 | __ceil__(...)
 | Округление в большую сторону целого возвращает его значение.

Кроме того также полезна dir(object), которая перечислит все доступные для объекта методы:

>>> dir(float)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

Наконец, относящаяся к объекту документация предоставляется функцией .__doc__, что показывает наш следующий пример:

>>> abs.__doc__
'Возвращает абсолютное значение своего аргумента.'

Синтаксис

Python не принимает терминаторы оператора, а блоки кода определяются через отступы. Операторы, которые ожидают некого уровня отступа должны завершаться двоеточием (:). Это приводит к тому, что:

Код Python яснее и лучше читается.

Имеющаяся структура программы всегда в соответствии с такими имеющимися отступами.

Установленный стиль отступов единообразен в любом листинге.

Плохие отступы могут повлечь за собой ошибки.

Наш следующий пример показывает как применяется конструкция if:

print("first print")
if condition:
    print(“second print”)
print(“third print”)

В приведённом примере мы можем наблюдать следующее:

Следующие операторы: print("first print"), if condition:, print("third print") имеют один и тот же уровень отступа и всегда выполняются.

После оператора if имеется некий блок кода с более высоким уровнем отступа, который содержит оператор print ("second print").

Когда условие if является истиной, оператор print ("second print") выполняется.

Когда условие if является ложью, оператор print ("second print") не выполняется.

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

Комментарии

Комментарий начинается с символа хэша (#) и расположен в одной строке:

# комментарий одной строкой

Для комментариев во множестве строк применяются строковые значения множественных строк:

""" первая строка комментария из множества строк
вторая строка комментария из множества строк."""

Присвоения

присвоение выполняется при помощи символа равенства (=). Для проверки эквивалентности применяется значение того же самого (==). Вы можете увеличивать или уменьшать некое значение при помощи операторов += и -= с идущей следом разницей. Это работает со многими типами данных, включая строки. В одной и той же строке вы можете выполнять присвоение множеству выражений.

Вот некоторые примеры:

>>> variable = 3
>>> variable += 2
>>> variable
5
>>> variable -= 1
>>> variable
4

>>> _string_ = "Hello"
>>> _string_ += " Parallel Programming CookBook Second Edition!"
>>> print (_string_)
Hello Parallel Programming CookBook Second Edition!

Типы данных

наиболее значимыми структурами в Python являются списки (lists), кортежи (tuples) и словари (dictionaries). Множества были интегрированы в Python начиная с версии 2.5 (для предыдущих версий они были доступны в соответствующей библиотеке sets):

Списки: Они аналогичны одномерным массивам, но вы также можете создавать списки, содержащие другие списки.

Словари: Это массивы, содержащие пару ключа и значения (хэш таблицы).

Кортежи: Это неизменяемые одномерные объекты.

Массивы могут иметь различные типы, поэтому вы можете смешивать переменные, такие как целые и строки в своих списках, словарях и кортежах.

Значение индекса самого первого объекта в любом виде массива всегда ноль. Допускаются отрицательные индексы и они исчисляются с самого конца данного массива; -1 указывает на самый последний элемент данного массива:

#давайте поиграем со списками
list_1 = [1, ["item_1", "item_1"], ("a", "tuple")]
list_2 = ["item_1", -10000, 5.01]

>>> list_1
[1, ['item_1', 'item_1'], ('a', 'tuple')]

>>> list_2
['item_1', -10000, 5.01]

>>> list_1[2]
('a', 'tuple')

>>>list_1[1][0]
['item_1', 'item_1']

>>> list_2[0]
item_1

>>> list_2[-1]
5.01

#построим некий словарь
dictionary = {"Key 1": "item A", "Key 2": "item B", 3: 1000}
>>> dictionary
{'Key 1': 'item A', 'Key 2': 'item B', 3: 1000}

>>> dictionary["Key 1"]
item A

>>> dictionary["Key 2"]
-1

>>> dictionary[3]
1000

при помощи символа двоеточия (:) вы можете задавать некий диапазон массива:

list_3 = ["Hello", "Ruvika", "how" , "are" , "you?"]
>>> list_3[0:6]
['Hello', 'Ruvika', 'how', 'are', 'you?']

>>> list_3[0:1]
['Hello']

>>> list_3[2:6]
['how', 'are', 'you?']

Строки

Строки Python выделяются при помощи символов одинарной (') или двойной (") кавычки, причём им дозволяется применение одной нотации внутри строки, ограниченной другой нотацией:

>>> example = "she loves ' giancarlo"
>>> example
"she loves ' giancarlo"

Для множества строк они заключаются в тройные кавычки (или три одинарных, '''строковое значение из множества строк''':

>>> _string_='''I am a
multi-line
string'''
>>> _string_
'I am a \nmulti-line\nstring'

Python также поддерживает Unicode; просто применяйте синтаксис u"This is a unicode string":

>>> ustring = u"I am unicode string"
>>> ustring
'I am unicode string

Для ввода в некой строке значений наберите значение оператора % и некий кортеж. Далее каждый оператор % замещается элементом кортежа, причём слева направо:

>>> print ("My name is %s !" % ('Mr. Wolf'))
My name is Mr. Wolf!

Управление потоком

Инструкциями управления потока выступают if, for, и while.

В своём следующем примере мы проверяем будет ли данное число положительным, отрицательным или нулём и отображаем полученный результат:

num = 1

if num > 0:
    print("Positive number")
elif num == 0:
    print("Zero")
else:
    print("Negative number")

Наш следующий блок кода находит сумму всех хранимых в неком списке чисел при помощи цикла for:

numbers = [6, 6, 3, 8, -3, 2, 5, 44, 12]
sum = 0
for val in numbers:
    sum = sum+val
print("The sum is", sum)

Для выполнения итераций в коде пока установленное условие истинно, мы будем выполнять оператор while. Мы будем применять такой цикл вместо цикла for, так как мы не знаем точное число итераций, которое будет в результате в данном коде. В своём примере мы применяем while для добавления натуральных чисел в sum = 1+2+3+...+n:

n = 10
# инициализируем sum и счётчик
sum = 0
i = 1
while i <= n:
    sum = sum + i
    i = i+1 # обновляем счётчик

# выводим на печать значение sum
print("The sum is", sum)

Вот вывод для наших предыдущих трёх примеров:

Positive number
The sum is 83
The sum is 55
>>>

Функции

Функции Python объявляются при помощи ключевого слова def:

def my_function():
    print("это некая функция")

Для запуска функции воспользуйтесь её названием со следующими за ним скобками:

>>> my_function()
это некая функция

Параметры должны определяться после самого названия функции, внутри скобок:

def my_function(x):
    print(x * 1234)

>>> my_function(7)
8638

Множество параметров необходимо разделять запятой:

def my_function(x,y):
    print(x*5+ 2*y)

>>> my_function(7,9)
53

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

def my_function(x,y=10):
    print(x*5+ 2*y)

>>> my_function(1)
25

>>> my_function(1,100)
205

Значения параметров функции могут иметь любой тип данных (например, строку, число, список и словарь). Здесь в качестве некого параметра для my_function применяется следующий список, lcities:

def my_function(cities):
    for x in cities:
        print(x)

>>> lcities=["Napoli","Mumbai","Amsterdam"]
>>> my_function(lcities)
Napoli
Mumbai
Amsterdam

Для возврата некого значения из функции пользуйтесь оператором return:

def my_function(x,y):
    return x*y

>>> my_function(6,29)
174

Python поддерживает интересный синтаксис, который позволяет вам задавать небольшие функции в одну строку прямо на лету. Имеющие своё происхождение из языка программирования Lisp, такие лямбда функции могут применяться везде где требуется некая функция.

Некий пример лямбда функции, functionvar, демонстрируется следующим образом:

# определения лямбда эквивалентно def f(x): return x + 1

functionvar = lambda x: x * 5
>>> print(functionvar(10))
50

Классы

Python поддерживает множество наследуемых классов. Условно (это не правило языка) частные переменные и методы объявляются предшествующим двойным нижним подчёркиванием (__). Мы можем назначать произвольные атрибуты (свойства) своим экземплярам класса, как это показано в нашем следующем примере:

class FirstClass:
    common_value = 10
    def __init__ (self):
        self.my_value = 100
    def my_func (self, arg1, arg2):
        return self.my_value*arg1*arg2

# Строим некий первый экземпляр
>>> first_instance = FirstClass()
>>> first_instance.my_func(1, 2)
200

# Строим второй экземпляр FirstClass
>>> second_instance = FirstClass()

#проверяем common_value для обоих своих экземпляров
>>> first_instance.common_value
10

>>> second_instance.common_value
10

#Изменяем common_value для своего first_instance
>>> first_instance.common_value = 1500
>>> first_instance.common_value
1500

#Как вы можете отметить, значение common_value для second_instance не изменилось
>>> second_instance.common_value
10

# SecondClass наследуется из FirstClass.
# множество экземпляров объявляются следующим образом:
# class SecondClass (FirstClass1, FirstClass2, FirstClassN)

class SecondClass (FirstClass):
    # The "self" argument is passed automatically
    # and refers to the class's instance
    def __init__ (self, arg1):
        self.my_value = 764
        print (arg1)

>>> first_instance = SecondClass ("hello PACKT!!!!")
hello PACKT!!!!

>>> first_instance.my_func (1, 2)
1528

Исключительные ситуации

Исключительные ситуации в Python управляются при помощи блоков try-except (exception_name)

def one_function():
     try:
         # Деление на ноль вызывает исключительную ситуацию
         10/0
     except ZeroDivisionError:
         print("Oops, error.")
     else:
         # Не было никакой исключительной ситуации, мы можем продолжить.
         pass
     finally:
         # Этот код выполняется после того, как блок
         # try..except уже был выполнен и все исключительные ситуации
         # были обработаны, даже когда некая новая
         # исключительная ситуация произошла в самом этом блоке.
         print("We finished.")

>>> one_function()
Oops, error.
We finished

Импорт библиотек

Внешние библиотеки импортируются при помощи import [library name]. В качестве альтернативы вы можете воспользоваться синтаксисом from [library name] import [function name] для импорта некой определённой функции. Вот некий образец:

import random
randomint = random.randint(1, 101)

>>> print(randomint)
65

from random import randint
randomint = random.randint(1, 102)

>>> print(randomint)
46

Управление файлами

Чтобы позволить нам взаимодействовать с имеющейся файловой системой, Python предоставляет нам свою встроенную функцию open. Данная функция вызывает открытие некого файла и возврат файлового объекта. Последний делает для нас возможным выполнение различных операций с таким файлом, например, чтение и запись. После того как мы заканчиваем взаимодействие с этим файлом, мы обязаны не забыть закрыть его при помощи соответствующего метода file.close:

>>>  f = open ('test.txt', 'w') # открываем необходимый файл для записи
>>>  f.write ('first line of file \ n') # записываем в файл строку
>>>  f.write ('second line of file \ n') # записываем в файл ещё однй строку
>>>  f.close () # мы закрываем этот файл
>>>  f = open ('test.txt') # повторно открываем этот файл на чтение
>>>  content = f.read () # считываем всё содержимое из этого файла
>>>  print (content)
first line of the file
second line of the file
>>>  f.close () # закрываем этот файл

Охватывающий список

Охватывающий список является мощным инструментом для создания списков и манипуляций с ними. Они составляются из некого выражения, за которым следует некий оператор for с последующими нулём или более операторов if. Необходимый для охватывающего списка синтаксис прост и выглядит так:

[expression for item in list]

Далее выполним следующее:

#Применяющий строки охватывающий список
>>> list_comprehension_1 = [ x for x in 'python parallel programming cookbook!' ]
>>> print( list_comprehension_1)

['p', 'y', 't', 'h', 'o', 'n', ' ', 'p', 'a', 'r', 'a', 'l', 'l', 'e', 'l', ' ', 'p', 'r', 'o', 'g', 'r', 'a', 'm', 'm', 'i', 'n', 'g', ' ', 'c', 'o', 'o', 'k', 'b', 'o', 'o', 'k', '!']

#Применяющий числа охватывающий список
>>> l1 = [1,2,3,4,5,6,7,8,9,10]
>>> list_comprehension_2 = [ x*10 for x in l1 ]
>>> print( list_comprehension_2)

[10, 20, 30, 40, 50, 60, 70, 80, 90, 100]

Запуск сценариев Python

Для выполнения некого сценария Python просто вызовите свой интерпретатор Pyton с последующим названием этого сценария, в нашем случае Codeю Или, когда мы в другом рабочем каталоге, тогда воспользуйтесь его полным адресом {Прим. пер.: или относительным}:

> python my_pythonscript.py

[Замечание]
Замечание

Начиная с данного момента и далее, для каждого вызова некого сценария Python мы будем применять предыдущую нотацию; то есть python с последующим script_name.py, в предположении что именно в том каталоге, из которого запущен наш интерперетатор Python, и расположен наш исполняемый сценарий.

Установка пакетов Python при помощи pip

pip это инструмент, позволяющий нам выполнять поиск, выгрузку и установку пакетов Python, поиск которых выполняется в установленном Python Package Index, который, в свою очередь, является неким репозиторием, содержащим десятки тысяч написанных на Python пакетов. Это делает для нас возможным управление пакетами, которые мы уже выгрузили, что позволяет нам обновлять и удалять их.

Установка pip

pip уже содержится в версиях Python ≥ 3.4 и ≥ 2.7.9. Чтобы проверить, установлен ли уже этот инструмент, мы можем запустить такую команду:

C:\>pip

Когда pip уже установлен, эта команда отобразит нам значение установленной версии.

Обновление pip

Также рекомендуется проверять что применяемая вами версия pip всегда современна. Для её обновления мы можем воспользоваться такой командой:

C:\>pip install -U pip

Применение pip

pip поддерживает некую последовательность команд, которые позволяют нам, помимо прочего, выполнять поиск(search), выгрузку (download), установку (install), обновление (update) и удаление (remove) пакетов.

Для установки PACKAGE, всего лишь вызовите такую команду:

C:\>pip install PACKAGE

Введение в параллельное программирование Python

Python предоставляет множество библиотек и инфраструктур, которые служат для удобства высоко производительных вычислений. Однако, выполнение параллельного программирования при помощи Python может быть достаточно коварным по причине GIL (Global Interpreter Lock, Глобальной блокировке интерпретатора).

На практике, большинство распространённых и наиболее широко применяемых интерпретаторов используют интерпретатор Python, CPython, разработанный на языке программирования C. Для интерпретатора CPython требуется GIL для сохраняющих поток (thread- safe) операций. Такое использование GIL подразумевает что вы подсчитываете глобальные блокировки всякий раз когда пытаетесь выполнить доступ к любому объекту Python, содержащемуся внутри потоков. Причём лишь один поток за раз способен выдать запрос на блокировку некого объекта Python или API C.

К счастью, дела не так плохи, потому что вне пределов этого царства GIL мы свободно можем использовать параллелизм. Эта категория содержит все те темы, которые мы обсудим с своих последующих главах, включая многопроцессность, распределённые вычисления и вычисления GPU.

Итак, Python в действительности не многопоточен. Однако что такое поток (thread)? Что такое процесс? В своём последующем разделе мы сделаем введение в эти два основополагающих понятия и то как они решаются в самом языке программирования Python. {Прим. пер.: с подробным обсуждением GIL можно ознакомиться в нашем переводе Полного руководства параллельного программирования на Python Куана Нгуена, вышедшей в ноябре 2018.}

Процессы и потоки

Потоки (threads) можно сопоставлять с облегчёнными процессами, в отношении того, что они предлагают подобные процессам преимущества, однако не требуя обычных технологий взаимодействия процессов. Потоки позволяют вам делить основной поток исполнения программы на множество одновременно исполняющихся управлений течений (stream). В отличие от них процессы обладают своим собственным адресным пространством и своими собственными ресурсами. Они следуют тому, что взаимодействие между частями кода, исполняющимся в различных процессах, может иметь место только через надлежащие механизмы управления, включая конвейеры, код FIFO, обмен почтовыми сообщениями, разделяемые области памяти и передачу сообщений. Потоки (threads), с другой стороны, делают возможным создание одновременно выполняющихся частей своей программы, в которых каждая из частей может осуществлять доступ к тем же самым адресному пространству, переменным и константам.

Ниже приводится таблица, суммирующая основные отличия между потоками и процессами:

Таблица 1-1. Сопоставление потоков и процессов

Потоки
Процессы

Совместно используют память

Не разделяют память

Запуск/ изменение менее затратны с точки зрения вычислений

Запуск/ изменение затратны с точки зрения вычислений

Требуют меньше ресурсов (облегчённые процессы)

Требуют больше вычислительных ресурсов

Нуждаются в механизмах синхронизации для корректной обработки данных

Не требуется синхронизация памяти

После краткого введения, мы, наконец, покажем как работают процессы и потоки.

В частности, мы бы желали сравнить значения времени последовательного, многопоточного и многопроцессного исполнения нашей следующей функции, do_something, которая выполняет какие- то базовые вычисления, содержащих построение списка выбираемых случайным образом целых (файл do_something.py):

import random

def do_something(count, out_list):
  for i in range(count):
    out_list.append(random.random())

Далее имеется некая последовательная реализация (serial_test.py). Давайте начнём с соответствующего импорта:

from do_something import *
import time

Обратите внимание, что мы импортировали модуль time, который будет применяться для оценки времени исполнения, причём именно в этом экземпляре, а также последовательную реализацию нашей функции do_something. size списка для построения равен 10000000, в то время как сама функция do_something будет исполнена 10 раз:

if __name__ == "__main__":
    start_time = time.time()
    size = 10000000
    n_exec = 10
    for i in range(0, exec):
        out_list = list()
        do_something(size, out_list)

    print ("List processing complete.")
    end_time = time.time()
    print("serial time=", end_time - start_time)

Далее у нас имеется надлежащая многопоточная реализация (multithreading_test.py).

Импортируем соответствующие библиотеки:

from do_something import *
import time
import threading

Обратите внимание на импортируемый модуль threading для работы с многопоточными возможностями в Python.

Здесь у нас присутствует многопоточное исполнение нашей функции do_something. Мы не более более глубоко комментировать все инструкции из своего следующего кода, так как они более подробно обсуждаются в Главе 2, Параллельность на основе потоков.

Однако, следует заметить, что в данном случае, также, длина получаемого списка очевидно та же самая, что и в нашем последовательном варианте, size = 10000000, в то время как число потоков задаётся равным 10, threads = 10, что также является числом исполнений нашей функции do_something:

if __name__ == "__main__":
    start_time = time.time()
    size = 10000000
    threads = 10
    jobs = []
    for i in range(0, threads):

Также обратите внимание на построение нашего отдельного потока через вызов метода threading.Thread:

out_list = list()
thread = threading.Thread(target=list_append(size,out_list))
jobs.append(thread)

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

from do_something import *
import time
import threading

Наконец, присутствует реализация множества процессов (multiprocessing_test.py).

Мы стартуем с импорта необходимых модулей, в частности, multiprocessing, свойства которого мы глубоко обсуждаем в Главе 3, Параллельность на основе процессов:

from do_something import *
import time
import multiprocessing

Как и в предыдущих случаях, общая длина списка построения, значение size, а также число выполнений функции do_something остаются теми же самыми (procs = 10):

if __name__ == "__main__":
    start_time = time.time()
    size = 10000000
    procs = 10
    jobs = []
    for i in range(0, procs):
        out_list = list()

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

process = multiprocessing.Process\
                  (target=do_something,args=(size,out_list))
        jobs.append(process)

Далее та последовательность циклов, в которых мы запускаем исполнение процессов и затем сразу после этого останавливаем их выполнение выглядит следующим образом:

for j in jobs:
        j.start()

    for j in jobs:
        j.join()

    print ("List processing complete.")
    end_time = time.time()
    print("multiprocesses time=", end_time - start_time)

После этого мы открываем свою командную оболочку и запускаем все три описанные ранее функции.

Перейдём в ту папку, куда мы скопировали эти функции и затем наберём следующее:

> python serial_test.py

Тот результат, который мы получили в машине со следующими характеристиками - ЦПУ Intel i7/8 ГБ ОЗУ, выглядит так:

List processing complete.
serial time= 25.428767204284668

В случае реализации multithreading мы имеем следующее:

> python multithreading_test.py

С последующим выводом:

List processing complete.
multithreading time= 26.168917179107666

Наконец, имеется реализация multiprocessing:

> python multiprocessing_test.py

Её результат таков:

List processing complete.
multiprocesses time= 18.929869890213013

Как мы можем видеть, полученные результаты нашей последовательной реализации (то есть, применения serial_test.py) аналогичны результатам, полученным в реализации со множеством потоков (при помощи multithreading_test.py), в которой на самом деле потоки запускаются один вслед за другим, отдавая приоритет одному над другим до конца, в то время как у нас имеется преимущество в терминах времени исполнения при применении возможностей множества потоков (через multiprocessing_test.py).

Глава 6. Распределённый Python

Содержание

Глава 6. Распределённый Python

Введение в распределённое программирование

Типы распределённых приложений

Клиент- серверные приложения

Клиент- серверная архитектура

Взаимодействие клиент- сервер

Архитектура TCP/IP клиент- сервер

Приложения со множеством уровней

Применение модуля Python socket

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Типы сокетов

Сокеты потоков

Дополнительно

Управление распределёнными задачами при помощи Celery

Приготовление

Установка Windows

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

RMI при помощи Pyro4

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Реализация топологии цепочки

Дополнительно

Данная глава предоставит введение в некоторые важные модули Python для распределённых вычислений. В частности, мы опишем модуль socket, который позволяет вам реализовывать простые приложения, распределяемые посредством модели клиент- сервер.

Затем мы сделаем введение в модуль Celery, который является мощной инфраструктурой Python, которая используется для управления распределёнными задачами. Наконец, мы опишем модуль Pyro4, позволяющий вам вызывать методы, которые используются в различных процессах, причём потенциально в различных машинах.

В этой главе мы рассмотрим такие рецепты:

Введение в распределённое программирование

Применение модуля Python socket

Управление распределённой задачей при помощи Celery

Вызов удалённого метода (RMI) с применением Pyro4

Введение в распределённое программирование

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

Когда ваша задача разделяется на множество частей меньшего размера, индивидуальные разделы этой задачи могут одновременно вычисляться большим числом процессоров. Это даёт возможность задействовать больше процессорной мощности над задачей, которая может быть предоставлена какому- то отдельному процессору.

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

Давайте посмотрим на имеющиеся другие различия:

Таблица 6-1. Сопоставление параллельной и распределённой обработки

Параллельная обработка
Распределённая обработка

Параллельная обработка имеет то преимущество, что предоставляет надёжную вычислительную мощность с очень низкой степенью латентности.

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

Сосредотачивая всю вычислительную мощность в одной системе, сводятся к минимуму обусловленные передачей данных потери в скорости.

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

Единственным реальным ограничением выступает общее число процессоров, содержащихся в такой системе.

Система почти неограниченно масштабируется так как нет практического верхнего ограничения на общее число процессоров для некой распределённой системы.

Однако, в контексте вычислительных приложений, обычно различают локальные и распределённые архитектуры:

Таблица 6-2. Локальные и распределённые архитектуры

Локальные архитектуры
Распределённые архитектуры

Все необходимые компоненты находятся в одной и той же машине.

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

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

Типы распределённых приложений

Распределённые приложения можно классифицировать согласно степени распределения:

Клиент- серверные приложения

Приложения со множеством уровней

Клиент- серверные приложения

Существует всего лишь два уровня тех операций, которые целиком обслуживаются в вашем сервере. В качестве примера мы можем упомянуть классические статические и динамические вебсайты. Основными инструментами для реализации таких видов приложений являются именно сетевые сокеты, программирование которых возможно на различных языках, включая C, C ++, Java и, конечно же, Python.

Терминология клиент- серверной системы относится к некой сетевой архитектуре, в которой компьютер или терминал клиента подключается к некому серверу для использования определённой службы; например, совместно с прочими клиентами использовать определённый аппаратный/ программного ресурса, или полагаться на лежащую в основе архитектуру протокола.

Клиент- серверная архитектура

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

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

Некое свойство, которое достигается клиентом от его сервера состоит в том, что этот клиент способен инициировать некую транзакцию со своим сервером, в то время как сам сервер может никогда не инициировать транзакции со своими клиентами по собственно инициативе:

Рисунок 6-1

Архитектура клиент- сервер

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

Взаимодействие клиент- сервер

Взаимодействие между клиентами и серверами может иметь место с применением различных механизмов - начиная с географически распределённых и локальных сетевых сред, вплоть до услуг коммуникации - между приложениями на уровне своей ОС. Более того, некая клиент- серверная архитектура должна быть независимой от того метода физического подключения, которое существует между рассматриваемым им клиентом и сервером.

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

Основным предметом в рассматриваемой архитектуре клиент- сервера, в плане управления данными, является предоставление приложениям клиента возможности доступа к управляемым его сервером сведениям. Такой сервер (понимаемый в неком логическом смысле как программное обеспечение) часто работает в некой удалённой системе (например, в другом городе или не в локальной сети).

Таким образом, клиент- серверные приложения часто ассоциируются с распределённой обработкой.

Архитектура TCP/IP клиент- сервер

Соединение TCP/IP устанавливает подключение точка- точка между двумя приложениями. Окончания этого соединения помечены неким IP адресом, который указывает необходимую рабочую станцию по номеру порта, который делает возможным наличие некоторого числа подключений, которые соединяются с различными приложениями в одной и той же рабочей станции.

После того как такое подключение установлено и имеющийся протокол может выполнять через него обмен данными, лежащий в его основе протокол TCP/IP заботится об отправке таких данных, разделяя их на пакеты, с одной стороны соединения к другой. В частности, протокол TCP имеет дело со сборкой и разборкой таких пакетов, а также с управлением взаимодействием (handshaking), которое обеспечивает надёжность самого соединения, в то время как протокол IP заботится о транспортировке всех индивидуальных пакетов и необходимом выборе наилучшего маршрута своих пакетов внутри имеющейся сетевой среды.

Такой механизм, составляет основу надёжности данного протокола TCP/IP, что, в свою очередь, представляет собой одну из причин развития самого этого протокола в военной сфере (ARPANET).

В различных имеющихся стандартных приложениях (таких как веб- просмотр, файловый обмен и обмен электронной почтой) применяют стандартизованные протоколы, например, HTTP, FTP, POP3, IMAP и SMTP.

Каждое клиент- серверное приложение должно вместо этого определять и применять свой собственный частный протокол приложения. Он может включать в себя необходимый обмен данными в блоках с фиксированным размером (что является самым простым решением)

{Прим. пер.: В современных системах, требующих максимально возможной скорости обмена, предоставляемой каналом связи, всё чаще вместо протокола IP в сетевых средах применяются другие протоколы, обладающие более высокой пропускной способностью, например UDP или RDMA. По большому счёту, всё излагаемое здесь применимо и к ним. Например.}

Приложения со множеством уровней

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

Лицевая сторона или уровень представления, или интерфейс

Средний уровень или логика приложения

Часть основы или уровень данных, или управление постоянными данными

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

Presentation Layer (PL, уровень представления): Это часть визуализации самих данных (например, модели и управление выводом) необходимые для устанавливаемого интерфейса пользователя.

Business Logic Layer (BLL, уровень запроса бизнес логики): Это основная часть рассматриваемого приложения, которая определяет различные логические составляющие и их взаимоотношения независимо от доступных пользователю методов представления и сохраняемого в архивах.

Data Access Layer (DAL, уровень доступа к данным): Содержит всё что требуется для необходимого управления постоянными данными (естественно, системы управления базой данных {Прим. пер.: так в тексте, на практике это могут быть и системы управления метаданными файловой системы, например, или основанных на API S3/ Swift системах хранения объектов}.)

Эта глава будет представлять некоторые предлагаемые Python решения для реализации распределённых архитектур. Мы начнём с описания модуля socket, при помощи которого мы сможем реализовать некие образцы основополагающих моделей клиент- сервер.

Применение модуля Python socket

Сокет представляет собой некий программный объект, который позволяет отправлять и получать данные между удалёнными хостами (через некую сетевую среду) или между локальными процессами, например, IPC (Inter-Process Communication, межпроцессное взаимодействие).

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

Основной успех распространения технологии сокетов идёт в ногу с развитием Интернета. На самом деле, именно сочетание сокетов с Интернетом создало взаимодействие между машинами любого типа и/ или разбросанных по всему миру, невообразимо простым (по крайней мере по сравнению с прочими системами).

Приготовление

Модуль socket Python выставляет API C нижнего уровня для взаимодействия поверх сетевой среды при помощи интерфейса сокета BSD (сокращение от Berkeley Software Distribution).

Этот модуль содержит класс Socket, который содержит самые основные методы для управления следующими задачами:

socket ([family [, type [, protocol]]]): строит сокеты при помощи таких аргументов:

Значения адреса family, которым может быть: AF_INET (значение по умолчанию), AF_INET6 или AF_UNIX

Значение type сокета, которыми могут быть SOCK_STREAM (значение по умолчанию), SOCK_DGRAM или возможно одна из прочих констант "AF_UNIX"

Значение номера protocol (которым обычно является нуль)

gethostname(): Возвращает текущее значение IP адреса этой машины.

accept(): Возвращает такую пару значений (conn и address), в которой conn выступает объектом типа сокета (для отправки, получения данных в этом соединении), в то время как address это значение адреса подключения к этому сокету с другой стороны данного соединения.

bind(address): Ассоциирует данный сокет со значением address сервера.

[Замечание]
Замечание

Этот метод исторически принимал пару параметров для значения адресов AF_INET вместо отдельного кортежа.

close(): Предоставляет возможность очистки данного соединения после того как взаимодействие с данным клиентом завершено. Такие сокеты закрываются и собираются сборщиком мусора.

connect(address): Подключает некий удалённый сокет с каим- то адресом. Представление формата address зависит от применяемого семейства адреса.

Как это сделать...

В нашем следующем примере применяемый сервер прослушивает установленный по умолчанию порт, а через соединение TCP/IP наш клиент отправляет в этот сервер данные и время установления подключения.

Вот реализация для server.py:

Импортируем относящиеся к делу модули Python:

import socket
import time

Создаём новый сокет применяя заданные адрес, тип сокета и номер протокола:

serversocket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

Получаем название локальной машины (host):

host=socket.gethostname()

Устанавливаем значение номера port:

port=9999

Подключаем (привязываем) свой сокет к host и port:

serversocket.bind((host,port))

Ожидаем осуществления подключения по данному порту. Значение нашего параметра 5 определяет максимальное число подключений в данной очереди. Это максимальное значение зависит от самой системы (обычно это 5), а минимальное значение всегда 0:

serversocket.listen(5)

Устанавливаем соединение:

while True:

Затем принимается установленное соединение. Возвращаемым значением является некая пара (conn, address), где conn это новый объект socket, который применяется для отправки и получения данных, а address это адрес подключёния к данному сокету. После принятия создан некий новый сокет и он будет обладать собственным идентификатором. Этот новый сокет применяется только для определённого клиента:

clientsocket,addr=serversocket.accept()

На печать выводятся значения подключённых адреса и порта:

print ("Connected with[addr],[port]%s"%str(addr))

Выполняется оценка currentTime:

currentTime=time.ctime(time.time())+"\r\n"

Наш следующий оператор отправляет данные в созданный сокет, возвращая общее число отправленных байт:

clientsocket.send(currentTime.encode('ascii'))

Наш следующий оператор указывает на закрытие сокета (то есть канала взаимодействия); все последующие операции с этим сокетом будут завершаться отказом. Все сокеты автоматически закрываются после того как они отвергнуты, но всегда рекомендуется закрывать их такой операцией close() :

clientsocket.close()

Следующий код служит для нашего клиента (client.py):

Импортируем библиотеку socket:

import socket

Далее создаётся объект socket:

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

Получаем название своей локальной машины (host):

host=socket.gethostname()

Настраиваем значение номера port:

port=9999

Устанавливаем соединение с host и port:

s.connect((host,port))

[Замечание]
Замечание

Максимальное значение байт, которые могут быть приняты не может превышать 1 024 байт (tm=s.recv(1024)).

Теперь закрываем своё соединение и в самом конце выводим на печать время данного подключения к серверу:

s.close()
print ("Time connection server:%s"%tm.decode('ascii'))

Как это работает...

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

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

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

Для запуска нашего примера стартуем свой сервер:

C:\> python server.py

Затем запускаем своего клиента (в другом терминале Windows):

C:\> python client.py

Получаемый на стороне клиента результат должен выдавать отчёт о подключаемых значениях адреса (addr) и порта (port):

Connected with[addr],[port]('192.168.178.11', 58753)

Однако на стороне сервера результат должен выглядеть как- то так:

Time connection server:Sun Mar 31 20:59:38 2019

Также ознакомьтесь...

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

Подлежащие передаче данные, которые содержатся в файле mytext.txt, побайтно копируются и отправляются в свой сервер через вызов функции conn.send. Этот сервер затем получает все данные и записывает их во второй файл, received.txt.

Исходный код для client2.py такой:

import socket
s =socket.socket()
host=socket.gethostname()
port=60000
s.connect((host,port))
s.send('HelloServer!'.encode())
with open('received.txt','wb') as f:
    print ('file opened')
    while True :
        print ('receiving data...')
        data=s.recv(1024)
        if not data:
            break
        print ('Data=>',data.decode())
        f.write(data)
f.close()
print ('Successfully get the file')
s.close()
print ('connection closed')

А это исходный код server2.py:

import socket
port=60000
s =socket.socket()
host=socket.gethostname()
s.bind((host,port))
s.listen(15)
print('Server listening....')
while True :
    conn,addr=s.accept()
    print ('Got connection from',addr)
    data=conn.recv(1024)
    print ('Server received',repr(data.decode()))
    filename='mytext.txt'
    f =open(filename,'rb')
    l =f.read(1024)
    while True:
        conn.send(l)
        print ('Sent',repr(l.decode()))
        l =f.read(1024)
        f.close()
        print ('Done sending')
        conn.send('->Thank you for connecting'.encode())
        conn.close()

Типы сокетов

Мы можем различать следующие три типа сокетов, которые задаются режимом соединения:

Сокеты Stream: это ориентированные на соединение сокеты и они основываются на протоколах надёжного подключения, таких как TCP или SCTP.

Сокеты Datagram: Это не ориентированные на установление соединения сокеты и они основываются на быстром, но не дающим гарантии протоколе UDP.

Сырые сокеты (raw IP): Он обходит транспортный уровень и значение заголовка доступно на неком уровне самого приложения.

Сокеты потоков

Мы более подробно остановимся только на этом типе сокета. Будучи основанном на транспортном уровне такого протокола как TCP, он гарантирует надёжное, полнодуплексное и ориентированное на установление соединения взаимодействие с неким потоком байт переменной длины.

Взаимодействие через этот сокет состоит из следующих фаз:

Создание сокета: Клиенты и серверы создают свои соответствующие сокеты и сервер выполняет ожидание по некому порту. Так как такой сервер способен создавать множество подключений с различными клиентами (а также и с тем же самым), ему требуется некая очередь для обработки различных запросов.

Запрос на подключение: Имеющиеся клиенты отправляют запросы на соединение со своим сервером. Обратите внимание на то, что у нас могут быть различными номера портов, так как один может назначаться лишь для исходящего обмена, а другой на вход. Это зависит от установленных настроек самого хоста. Естественно, значение локального порта клиента не обязательно должно совпадать со значением порта его удалённого сервера. Установленный сервер получает соответствующий запрос и, если он принимается, создаётся некое новое соединение. На приводимой схеме значением порта сокета клиента выступает 8080, в то время как значением порта сервера является 80.

Взаимодействие: Теперь наши клиент и сервер взаимодействуют через некий виртуальный канал между созданным сокетом клиента и новым сокетом (со стороны сервера), созданным специально для необходимого потока данных: сокета данных. Как уже было упомянуто на самой первой фазе, наш сервер создаёт такой сокет данных, так как его первый сокет данных применяется исключительно для запросов управления. Таким образом становится возможным для работы с этим сервером множества клиентов, причём каждый с соответствующим сокетом данных, намеренно создаваемым для него сервером.

Закрытие соединения: Поскольку TCP является протоколом с установлением соединения, когда больше нет необходимости во взаимодействии, наш клиент сообщает об этом своему серверу, который открепляет его сокет данных.

Основные фазы взаимодействия через сокет потока сведены в следующую схему:

Рисунок 6-2

Фазы сокета Stream

Дополнительно

Дополнительные сведения о сокетах Python можно найти по следующей ссылке.

Управление распределёнными задачами при помощи Celery

Celery является инфраструктурой Python, которая управляет распределёнными задачами приводимым ниже объектно- ориентированным подходом программного обеспечения (ПО) среднего уровня. Его главная функциональность состоит в обработке большого числа небольших задач и их распределения по многим вычислительным узлам. В конце концов, получаемые каждой задачей результаты будут затем переработаны чтобы составить общее решение.

Для применения Celery требуется брокер сообщений. Это некий независимый (от Celery) программный компонент, который обладает функциональностью программного обеспечения промежуточного уровня и который используется для отправки и получения сообщений для распределения задач по исполнителям (workers).

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

Эталонная архитектура, в которой имеющийся брокер сообщений управляет их обменом, основана на так называемой концепции публикации, подписки, которая выглядит следующим образом:

Рисунок 6-3

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

Celery поддерживает множество видов брокеров. Однако наиболее совершенными являются RabbitMQ и Redis.

Приготовление

Для инсталляции Celery воспользуйтесь установщиком pip следующим образом:

C:\> pip install celery

Затем необходимо установить брокера сообщений. Имеются доступными несколько возможностей, однако для нашего примера мы рекомендуем установить RabbitMQ со следующей ссылки.

[Замечание]
Замечание

RabbitMQ является ПО промежуточного уровня для обмена сообщениями, который реализует протокол AMQP (Advanced Message Queuing Protocol). Сам сервер RabbitMQ написан на языке программирования Erlang, поэтому для его установки вам потребуется установить и Erlang, выгрузив его со ссылки.

Необходимые шаги таковы:

Для проверки правильности установки celery вначале запустите необходимый брокер сообщений (например, RabbitMQ). А затем наберите следующее:

C:\> celery --version

Приводимый ниже вывод указывает какая именно версия celery установлена:

4.2.2 (Windowlicker)

Далее мы изучим как создавать некую задачу при помощи модуля celery.

celery предоставляет для вызова задачи два таких метода:

apply_async(args[, kwargs[, ...]]): Он отправляет некое сообщение с задачей.

delay(*args, **kwargs): Это сокращение для отправки некого сообщения с задачей, однако оно само по себе не сопровождает вариант исполнения.

[Замечание]
Замечание

Метод delay проще применять, так как он вызывается как обычная функция: task.delay(arg1, arg2, kwarg1='x', kwarg2='y'). В то время как для apply_async синтаксис таков: task.apply_async(args=[arg1,arg2] kwargs={'kwarg1':'x','kwarg2': 'y'}) .

Установка Windows

Для использования Celery в среде Windows, вы должны выполнить такие процедуры:

Проследовать в System Properties | Environment Variables | User or System variables | New.

Установить следующие значения:

Имя переменной: FORKED_BY_MULTIPROCESSING

Значение переменной: 1

Основной причиной для данной установки выступает зависимость Celery от пакета billiard, который пользуется переменной FORKED_BY_MULTIPROCESSING.

Для дополнительных сведений относительно установки Celery в Windows ознакомьтесь со следующими материалами.

Как это сделать...

Наша задача состоит в суммировании двух чисел. Для осуществления этой простой задачи нам придётся скомпоновать файлы сценариев addTask.py и addTask_main.py.

Для addTask.py мы начинаем с импорта инфраструктуры Celery:

from celery import Celery

Далее определяем саму задачу. В нашем примере это сумма двух чисел:

app = Celery('tasks', broker='amqp://guest@localhost//')
@app.task
def add(x, y):
    return x + y

Теперь импортируем файл addTask.py, который был определён ранее в addtask_main.py:

import addTask

После этого вызываем addTask.py для нахождения суммы двух чисел:

if __name__ == '__main__':
    result = addTask.add.delay(5,5)

Как это работает...

Чтобы применять Celery, прежде всего требуется запустить службу RabbitMQ, а затем запустить сервер исполнения Celery (то есть наш файл сценария addTask.py), набрав следующее:

C:\> celery -A addTask worker --loglevel=info

Вывод будет примерно таким:

Microsoft Windows [Versione 10.0.17134.648]
(c) 2018 Microsoft Corporation. Tutti i diritti sono riservati.

C:\Users\Giancarlo>cd C:\Users\Giancarlo\Desktop\Python Parallel Programming CookBook 2nd edition\Python Parallel Programming NEW BOOK\chapter_6 - Distributed Python\esempi

C:\Users\Giancarlo\Desktop\Python Parallel Programming CookBook 2nd edition\Python Parallel Programming NEW BOOK\chapter_6 - Distributed Python\esempi>celery -A addTask worker --loglevel=info

 -------------- celery@pc-giancarlo v4.2.2 (windowlicker)
---- **** -----
--- * *** * -- Windows-10.0.17134 2019-04-01 21:32:37
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app: tasks:0x1deb8f46940
- ** ---------- .> transport: amqp://guest:**@localhost:5672//
- ** ---------- .> results: disabled://
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker)
--- ***** -----
 -------------- [queues]
                .> celery exchange=celery(direct) key=celery
[tasks]
  . addTask.add

[2019-04-01 21:32:37,650: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2019-04-01 21:32:37,745: INFO/MainProcess] mingle: searching for neighbors
[2019-04-01 21:32:39,353: INFO/MainProcess] mingle: all alone
[2019-04-01 21:32:39,479: INFO/SpawnPoolWorker-2] child process 10712 calling self.run()
[2019-04-01 21:32:39,512: INFO/SpawnPoolWorker-3] child process 10696 calling self.run()
[2019-04-01 21:32:39,536: INFO/MainProcess] celery@pc-giancarlo ready.
[2019-04-01 21:32:39,551: INFO/SpawnPoolWorker-1] child process 6084 calling self.run()
[2019-04-01 21:32:39,615: INFO/SpawnPoolWorker-4] child process 2080 calling self.run()

Затем при помощи Python запускается второй сценарий:

C:\> python addTask_main.py

Наконец, в своём первом приглашении Командной строки мы получаем следующий результат:

[2019-04-01 21:33:00,451: INFO/MainProcess] Received task: addTask.add[6fc350a9-e925-486c-bc41-c239ebd96041]
[2019-04-01 21:33:00,452: INFO/SpawnPoolWorker-2] Task addTask.add[6fc350a9-e925-486c-bc41-c239ebd96041] succeeded in 0.0s: 10

Как вы можете видеть, полученным результатом является 10. Давайте сосредоточимся на своём первом сценарии, addTask.py: в первых двух строках кода мы создали некий экземпляр приложения Celery, который использует установленную службу брокера RabbitMQ:

from celery import Celery
app = Celery('addTask', broker='amqp://guest@localhost//')

Самым первым параметром в нашей функции Celery является название текущего модуля (addTask.py), а вторым параметром выступает аргумент распределительного щита брокера; он указывает значение URL, которое применяется для соединения с установленным брокером (RabbitMQ).

Теперь давайте введём подлежащую исполнению задачу.

Всякую задачу следует добавлять с помощью аннотации @app.task (а именно, применяя декоратор); такой декоратор помогает Celery определять какую именно функцию следует планировать в его очереди задач.

После такого декоратора мы создаём необходимую задачу, которую способны выполнять исполнители: это будет некая простая функция, которая вычисляет сумму двух чисел:

app.task
def add(x, y):
    return x + y

В своём втором сценарии, addTask_main.py, мы вызываем нашу задачу с помощью метода delay():

if __name__ == '__main__':
    result = addTask.add.delay(5,5)

Напоминаем вам, что этот метод является закладкой для имеющегося метода apply_async(), что предоставляет нам дополнительный контроль над исполнением нашей задачи.

Также ознакомьтесь...

Применение Celery очень простое. Для этого можно пользоваться такими командами:

Usage: celery <command> [options]

Вот её параметры:

positional arguments:
  args

optional arguments:
  -h, --help             show this help message and exit
  --version              show program's version number and exit

Global Options:
  -A APP, --app APP
  -b BROKER, --broker BROKER
  --result-backend RESULT_BACKEND
  --loader LOADER
  --config CONFIG
  --workdir WORKDIR
  --no-color, -C
  --quiet, -q

Основные команды следующие:

+ Main:
| celery worker
| celery events
| celery beat
| celery shell
| celery multi
| celery amqp

+ Remote Control:
| celery status

| celery inspect --help
| celery inspect active
| celery inspect active_queues
| celery inspect clock
| celery inspect conf [include_defaults=False]
| celery inspect memdump [n_samples=10]
| celery inspect memsample
| celery inspect objgraph [object_type=Request] [num=200 [max_depth=10]]
| celery inspect ping
| celery inspect query_task [id1 [id2 [... [idN]]]]
| celery inspect registered [attr1 [attr2 [... [attrN]]]]
| celery inspect report
| celery inspect reserved
| celery inspect revoked
| celery inspect scheduled
| celery inspect stats

| celery control --help
| celery control add_consumer <queue> [exchange [type [routing_key]]]
| celery control autoscale [max [min]]
| celery control cancel_consumer <queue>
| celery control disable_events
| celery control election
| celery control enable_events
| celery control heartbeat
| celery control pool_grow [N=1]
| celery control pool_restart
| celery control pool_shrink [N=1]
| celery control rate_limit <task_name> <rate_limit (e.g., 5/s | 5/m |
5/h)>
| celery control revoke [id1 [id2 [... [idN]]]]
| celery control shutdown
| celery control terminate <signal> [id1 [id2 [... [idN]]]]
| celery control time_limit <task_name> <soft_secs> [hard_secs]

+ Utils:
| celery purge
| celery list
| celery call
| celery result
| celery migrate
| celery graph
| celery upgrade

+ Debugging:
| celery report
| celery logtool

+ Extensions:
| celery flower
-------------------------------------------------------------

При помощи Webhooks протокол Celery можно реализовать в любом языке программирования.

Дополнительно

Дополнительные сведения по Celery можно найти на странице его проекта.

Рекомендуемыми брокерами сообщений являются: RabbitMQ и Redis. Кроме того имеются MongoDB, Beanstalk, Amazon SQS, CouchDB и IronMQ.

{Прим. пер.: рекомендуем также свои переводы RabbitMQ для профессионалов Гайвина Роя, Manning Publications Co., сентябрь 2017; большого числа глав из Книга рецептов Redis 4.x Пеньчень Хуань, Зуофей Вань, Packt Publishing, февраль 2018; а также ряда глав из Практического программирование MQTT на Python Гастона К. Хайляра, Packt Publishing, май 2018 и Копирки для программирования на Python Даниеля Фуртейдо, Маркуса Пеннингтона, Packt Publishing, февраль 2018.}

RMI при помощи Pyro4

Pyro это сокращение для Python Remote Objects. Он работает в точности как Java RMI (сокращение для Remote Method Invocation), позволяя вызывать некий метод какого- то удалённого объекта (относящегося к другому процессу) в точности как если бы такой объект был бы локальным (и относился бы к тому же самому процессу, в котором вызвано это исполнение).

Само применение механизма RMI для некой объектно- ориентированной системы сулит значительные преимущества унификации и симметричности в вашем проекте; так как такой механизм позволяет возможность моделирования взаимодействия между распределёнными процессами при помощи одного и того же концептуального инструментария.

Как вы можете увидеть на следующей схеме, Pyro4 делает возможным распределение объектов в неком стиле клиент, сервер; это означает что основная часть системы Pyro4 может переключаться с какого- то вызывающего клиента на некий удалённый объект, который вызывается на обслуживание какой- то функции:

Рисунок 6-4

RMI

Важно отметить, что в процессе удалённого вызова существуют две различающиеся части: некий клиент и какой- то сервер, который принимает и выполняет вызов своего клиента.

Приготовление

Весь метод управления данным механизмом неким распределённым образом целиком предоставляется Pyro4. Для установки самого последнего выпуска Pyro4 воспользуйтесь установщиком pip (в нашем случае применяется установка в Windows) и добавьте такую команду:

C:\> pip install Pyro4

Для данного рецепта мы воспользуемся кодами pyro_server.py и pyro_client.py.

Как это сделать...

В этом примере мы рассмотрим как построить и применить простейшее взаимодействие клиент- сервер с использованием программного обеспечения (ПО) промежуточного уровня Pyro4. Вот код для сервера pyro_server.py:

Импортируем библиотеку Pyro4:

import Pyro4

Определяем класс Server, который содержит подлежащий выставлению метод welcomeMessage():

class Server(object):
    @Pyro4.expose
    def welcomeMessage(self, name):
        return ("Hi welcome " + str (name))

[Замечание]
Замечание

Обратите внимание на декоратор, @Pyro4.expose, означающий что наш предыдущий метод будет доступен удалённо.

Функция startServer содержит все инструкции, которые применяются для запуска необходимого сервера:

def startServer():

Далее строим экземпляр server класса Server:

server = Server()

После этого определяем демона Pyro4:

daemon = Pyro4.Daemon()

Для выполнения данного сценария нам следует запустить некий оператор Pyro4 для определения местоположения некого сервера имён:

ns = Pyro4.locateNS()

Регистрируем свой объект сервера как Pyro object; он будет известен только в демоне Pyro:

uri = daemon.register(server)

Теперь мы имеем возможность регистрации своего объекта сервера с неким именем в своём сервере имён:

ns.register("server", uri)

Эта функция завершается вызовом метода requestLoop установленного демона. Это запускает необходимый цикл событий данного сервера и ожидания вызовов:

print("Ready. Object uri =", uri)
daemon.requestLoop()

Наконец, вызываем startServer через свою программу main:

if __name__ == "__main__":
    startServer()

Вот код для клиента pyro_client.py:

Импортируем библиотеку Pyro4:

import Pyro4

API библиотеки Pyro4 позволяет разработчику распределять объекты неким прозрачным образом. В данном примере наш сценарий клиента отправляет запросы в программу своего сервера для выполнения метода welcomeMessage():

uri = input("What is the Pyro uri of the greeting object? ").strip()
name = input("What is your name? ").strip()

Затем создаётся сам удалённый вызов:

server = Pyro4.Proxy("PYRONAME:server")

Наконец, наш клиент вызывает свой сервер, выводя на печать сообщение:

print(server.welcomeMessage(name))

Как это работает...

Наш предыдущий пример состоит из двух основных функций: pyro_server.py и pyro_client.py.

В pyro_server.py объект класса Server предоставляет свой метод welcomeMessage(), возвращающий некую строку, идентичную значению имени, вставляемого в данном сеансе клиента:

class Server(object):
    @Pyro4.expose
    def welcomeMessage(self, name):
        return ("Hi welcome " + str (name))

Pyro4 использует для диспетчеризации входящих вызовов по надлежащим объектам объекты демонов. Некий сервер обязан создать лишь одного демона, который управляет всем из такого экземпляра. Каждый сервер обладает демоном, которому известно обо всех имеющихся объектах Pyro, предоставляемых этим сервером:

daemon = Pyro4.Daemon()

Что касается самой функции pyro_client.py, вначале выполняется необходимый удалённый вызов и создаётся какой- то объект Proxy. В частности, клиент Pyro4 применяет объекты посредников (прокси) для передачи вызовов методов в необходимые удалённые объекты, а затем передаёт получаемые результаты обратно в код вызова:

server = Pyro4.Proxy("PYRONAME:server")

Для выполнения некого клиент- серверного соединения нам требуется иметь запущенным некий сервер имён Pyro4. В приглашении командной строки наберите следующее:

C:\> python -m Pyro4.naming

После этого вы обнаружите такое сообщение:

Not starting broadcast server for localhost.
NS running on localhost:9090 (127.0.0.1)
Warning: HMAC key not set. Anyone can connect to this server!
URI = PYRO:Pyro.NameServer@localhost:9090

Наше предыдущее сообщение означает, что данный сервер имён запуще в вашей сетевой среде. Наконец, мы можем запустить свой сервер и необходимые сценарии клиентов в двух отдельных консолях Windows:

Чтобы запустить pyro_server.py просто наберите:

C:\> python pyro_server.py

Вслед за этим вы обнаружите нечто такое:

Ready. Object uri =
PYRO:obj_76046e1c9d734ad5b1b4f6a61ee77425@localhost:63269

Затем запустите клиента набрав:

C:\> python pyro_client.py

На печать будет выведено сообщение:

What is your name?

Вставьте некое имя, например, Ruvika:

What is your name? Ruvika

Будет отображено следующее сообщение:

Hi welcome Ruvika

Также ознакомьтесь...

Помимо самих функциональных возможностей Pyro4, также существует необходимость создания топологий объектов. Например, давайте предположим что мы желаем построить некую распределённую архитектуру, которая следует какой- то цепной топологии, как показано ниже:

Рисунок 6-5

Соединение объектов в цепь при помощи Pyro4

Наш клиент выполняет запрос к Server 1, а далее запрос отправляется к Server 2, который после этого вызывает Server 3. Данная цепь вызовов завершается когда Server 3 вызывает Server 1.

Реализация топологии цепочки

Для реализации некой цепной технологии с помощью Pyro4, нам требуется реализовать какой- то объект chain, а также объекты client и server. Класс Chain делаем возможным перенаправление вызова к следующему серверу в последовательности путём обработки входного сообщения и перестройки установленного адреса сервера к которому следует адресовать данный запрос.

Также обратите внимание, что в данном случае применяется декоратор @Pyro4.expose, который позволяет выставление всех методов данного класса (chainTopology.py):

import Pyro4

@Pyro4.expose
class Chain(object):
    def __init__(self, name, next_server):
        self.name = name
        self.next_serverName = next_server
        self.next_server = None

    def process(self, message):
        if self.next_server is None:
            self.next_server = Pyro4.core.Proxy("PYRONAME:example.\
                chainTopology." + self.next_serverName)

Когда цепочка закрывается (выполняется самый последний вызов от server_chain_3.py к server_chain_1.py), тогда на печать выводится вызвавшее закрытие сообщение:

if self.name in message:
            print("Back at %s;the chain is closed!" % self.name)
            return ["complete at " + self.name]

Переправлямое сообщение выводится на печать когда в данной цепи имеется некий следующий элемент:

else:
            print("%s forwarding the message to the object %s" %\
                (self.name, self.next_serverName))
            message.append(self.name)
            result = self.next_server.process(message)
            result.insert(0, "passed on from " + self.name)
            return result

Далее у нас имеется исходный код для нашего клиента (client_chain.py):

import Pyro4

obj = Pyro4.core.Proxy("PYRONAME:example.chainTopology.1")
print("Result=%s" % obj.process(["hello"]))

Вслед за этим имеется исходный код для нашего первого сервера (server_1) в данной цепи, который вызывается из нашего клиента (server_chain_1.py). Здесь импортируются относящиеся к делу библиотеки. Обратите внимание на файл chainTopology.py, который был описан ранее.

import Pyro4
import chainTopology

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

current_server= "1"
next_server = "2"

Остальные строки кода определяют необходимое взаимодействие со следующем элементом в общей цепи:

servername = "example.chainTopology." + current_server
daemon = Pyro4.core.Daemon()
obj = chainTopology.Chain(current_server, next_server)
uri = daemon.register(obj)
ns = Pyro4.locateNS()
ns.register(servername, uri)
print("server_%s started " % current_server)
daemon.requestLoop()

Для исполнения данного примера вначале запускаем необходимый сервер имён Pyro4:

C:\> python -m Pyro4.naming
Not starting broadcast server for localhost.
NS running on localhost:9090 (127.0.0.1)
Warning: HMAC key not set. Anyone can connect to this server!
URI = PYRO:Pyro.NameServer@localhost:9090

Запускаем свои три сервера в трёх различных терминальных окнах, набирая каждый из них соответствующим образом (в нашем случае применяются терминалы Windows).

Самый первый сервер (server_chain_1.py) расположен в первом терминале:

C:\> python server_chain_1.py

Вслед за ним второй сервер (server_chain_2.py) во втором терминале:

C:\> python server_chain_2.py

И, наконец, третий сервер (server_chain_3.py) в третьем терминале:

C:\> python server_chain_3.py

После этого ещё в одном терминале запускаем сценарий client_chain.py:

C:\> python client_chain.py

Это приводит к такому выводу в приглашении его Командной строки:

Result=['passed on from 1','passed on from 2','passed on from 3','complete at 1']

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

Кроме того мы сосредоточимся на поведении самих объектов серверов в процессе передачи ими запроса в свой следующий в цепи объект (отсчитывая со стартового сообщения):

Запускается server_1 и его сообщение передаётся в server_2:

server_1 started
1 forwarding the message to the object 2

server_2 передаёт сообщение в server_3:

server_2 started
2 forwarding the message to the object 3

server_3 передаёт сообщение в server_1:

server_3 started
3 forwarding the message to the object 1

Наконец, это сообщение возвращается в точку отправления (иными словами, в server_1), закрывая данную цепочку:

server_1 started
1 forwarding the message to the object 2
Back at 1; the chain is closed!

Дополнительно

Документация Pyro4 доступна по ссылке.

Она содержит описание и некие примеры для выпуска 4.75.

Глава 7. Облачные вычисления

Содержание

Глава 7. Облачные вычисления

Что такое облачные вычисления?

Основы архитектуры облачных вычислений

Модели служб

SaaS

PaaS

IaaS

Модели распределения

Общедоступное облако

Частное облако

Облачное сообщество

Гибридное облако

Платформы облачных вычислений

Разработка веб приложений при помощи PythonAnywhere

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Докеризация приложения Python

Приготовление

Установка Docker for Windows

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Введение в вычисления без серверов

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Что такое функция Лямбда?

Почему без сервера?

Возможные проблемы и ограничения

Дополнительно

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

Прежде всего мы опишем платформу PythonAnywhere, в которой мы развернём облачное приложение Python. В контексте облачных вычислений будут обозначены две развивающиеся технологии: контейнеры и безсерверная технология.

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

На самом деле вам не стоит беспокоиться относительно представления, сереверов или конфигураций необходимой инфраструктуры. Вам только придётся создавать функции (а именно, Лямбда функции), которые способны работать независимо от таких приложений.

В этой главе мы рассмотрим такие вопросы:

Что такое облачные вычисления?

Основные моменты архитектуры облачных вычислений

Разработка веб приложений при помощи PythonAnywhere

Докеризация приложения Python

Введение в безсерверные вычисления

Мы также рассмотрим как получать преимущества от инфраструктуры AWS Lambda при разработке приложений Python.

Что такое облачные вычисления?

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

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

В частности, пять имеющихся существенных характеристик таковы:

Свободный и предоставляемый по запросу доступ: Это делает для пользователей возможным доступ - через дружественные пользователю интерфейсы - к тем службам, которые предлагаются поставщиком, причём без вмешательства персонала.

Повсеместный доступ к необходимой сетевой среде: Доступ к ресурсам осуществляется через имеющуюся сетевую среду и может осуществляться через стандартные устройства - такие как смартфоны, планшеты и персональные компьютеры.

Быстрая эластичность: Это способность рассматриваемого облачного решения увеличивать или уменьшать выделенные ресурсы быстрым и автоматическим образом, причём так, что пользователям будет казаться что они безграничны. Это предоставляет великолепную масштабируемость всей системе.

Служба измерения: Облачные системы постоянно выполняют мониторинг всех предлагаемых ресурсов и автоматически оптимизируют их на основании получаемых оценок использования. Тем самым все пользователи оплачивают только те ресурсы, которые реально применяются в таком определённом сеансе.

Совместное использование ресурсов: Сам поставщик предоставляет свои ресурсы через модель со множеством арендаторов с тем, чтобы они были способны выделяться и убираться динамически, на основании поступающих запросов пользователя и использоваться множеством потребителей:

Рисунок 7-1

Основные свойства облачных вычислений

Тем не менее, существует множество определений облачных вычислений, причём каждое из них обладает различными интерпретациями и значениями. NIST (National Institute of Standards and Technology) попытался предоставить подробное и официальное объяснение.

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

Наиболее общими подходами к виртуализации являются такие:

Контейнеры

Виртуальные машины

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

Наиболее распространённым приложением виртуализации через контейнеры выступает Docker. Мы сделаем краткое введение в эту инфраструктуру и рассмотрим как упаковывать в контейнеры (или выполнять докеризацию) некого приложения Python.

Основы архитектуры облачных вычислений

Имеющаяся архитектура облачных вычислений ссылается на ряд компонентов и их составных частей, которые и составляют общую структуру самой системы. Обычно их можно сгруппировать на два основных раздела, Внешний и Внутренний (Frontend и Backend):

Рисунок 7-2

Архитектура облачных вычислений

Каждый раздел обладает очень специфичными смыслами и сферами и соединяется с прочими через некую выиртуальную сеть или сетевую среду интернета.

Внешний интерфейс ссылается на ту часть облачных вычислений, которая видна его пользователю, которая реализуется через последовательность интерфейсов и приложений, позволяющих их потребителю выполнять доступ к рассматриваемой облачной системе. Различные облачные вычислительные системы обладают различными UI (User Interface, Интерфейсами пользователя).

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

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

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

Модели служб

Все предлагаемые службы облачных вычислений распадаются на три широкие категории:

SaaS - Software as a Service

PaaS - Platform as a Service

IaaS - Infrastructure as a Service

Эта классификация ведёт к определению некой схемы имеет название модели SPI (см. выделенные Жирным заглавные буквы из предыдущего перечня). Порой она именуется также стеком облачных вычислений, поскольку эти категории основываются одна на другой.

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

SaaS

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

Существует множество преимуществ использования такой модели как для пользователя, так и для самого поставщика. Для конкретного пользователя имеется значительное снижение в стоимости управления, а для поставщика решения имеется дополнительный контроль над всем обменом, позволяя тем самым ему избегать каких бы то ни было перекрытий. Неким примером SaaS выступают любые службы электронной почты на основе веб интерфейса, такие как Gmail, Outlook, Salesforce и Yahoo!.

PaaS

В отличии от SaaS, эти службы относятся ко всей среде разработки некого приложения, а не просто к его использованию. Таким образом, решение PaaS предоставляет некую облачную платформу, которая доступна через веб браузер для необходимых разработки, тестирования и управления программными приложениями. Более того, сам поставщик предоставляет интерфейсы на веб основе, архитектуру со множеством арендаторов и инструменты взаимодействия чтобы позволить разработчикам создавать приложения простым образом. Это поддерживает весь жизненный цикл такого программного обеспечения, а также поощряет к содружеству.

Примерами PaaS являются Microsoft Azure Services, Google App Engine и Amazon Web Services.

IaaS

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

Такая модель используется как небольшими развивающимися компаниями, которые не обладают значительным капиталом для инвестиций, а также уже состоявшимися компаниями, ищущими обтекаемой формы своих аппаратных архитектур. Имеющийся диапазон продавцов IaaS чрезвычайно широк и включает Amazon Web Services, IBM и Oracle.

Модели распределения

Архитектуры облачных вычислений не являются одинаковыми. По существу, имеется четыре модели распространения:

Public cloud - ощедоступные облака

Private cloud - частные облака

Cloud community - облачные сообщества

Hybrid cloud - Гибридные облака

Общедоступное облако

Эта модель распределения доступна всем, как индивидуальным пользователям, так и компаниям. Обычно такое общедоступное облако запущено в неком Центре обработки данных (ЦОД, DC), которым владеет поставщик услуг, обслуживающий оборудование, программные средства и прочие инфраструктуры поддержки. Таким образом, конечный пользователь освобождён от каких бы то ни было действий/ затрат на сопровождение.

Частное облако

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

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

Облачное сообщество

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

Гибридное облако

NIST определил его как результат композиции всех трёх упомянутых выше моделей реализации (частной, общедоступной и облачного сообщества) в попытке получения преимуществ каждой из трёх чтобы компенсировать прочие слабые стороны. Применяемые облака остаются различными объектами, а это может приводить к отсутствию операционной согласованности. Поэтому перед компаниями, которые используют данную модель, лежит задача обеспечения за счёт частных технологий совместимости их серверов, оптимизации их под определённые роли, которые они должны выполнять.

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

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

Платформы облачных вычислений

Платформы облачных вычислений являются наборами программного обеспечения и технологий, которые включают доставку ресурсов в само облако (по запросу, масштабируемо и в виде виртуальных ресурсов). Среди наиболее популярных платформ - платформы Google и, естественно, веха облачных вычислений: AWS (Amazon Web Services). Обе поддерживают Python в качестве языка программирования для разработки.

Тем не менее, в своём следующем рецепте мы сосредоточимся на PythonAnywhere, который является некой облачной платформой разработки специально для развёртывания веб приложений на языке программирования Python.

Разработка веб приложений при помощи PythonAnywhere

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

Более того, эта среда разработки также позволяет вам выбирать с какой версией Python работать. В ней простой мастер помогает нам с предварительной настройкой некого приложения.

Приготовление

Для началя давайте получим полномочия регистрации на этом сайте.

Приводимый ниже снимок экрана показывает различные типы подписок, а также возможность получения бесплатной учётной записи (пройдите, пожалуйста, по ссылке):

Рисунок 7-3

PythonAnywhere: регистрационная страница

После получения доступа к данной площадке (мы рекомендуем вам создать некую учётную запись начинающего), мы регистрируемся. Принимая во внимание, что интегрированные в браузер оболочки Python очень полезны, в особенности для начинающих и для вводных курсов по программированию, они, несомненно, не новы с технологической точки зрения.

Вместо этого, дополнительная ценность PythonAnywhere начинает ощущаться сразу после того как вы входите в систему, получая доступ к персональной панели инструментов:

Рисунок 7-4

PythonAnywhere: инструментальная панель

Посредством этой персональной панели инструментов мы можем выбрать какую именно версию Python запускать из 2.7 и 3.7, причё с интерфейсом IPython или без него:

Рисунок 7-5

PythonAnywhere: консоль просмотра

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

Рисунок 7-6

PythonAnywhere: оболочка Python

В своём следующем разделе мы хотим показать вам как применять PythonAnywhere для написания простого веб приложения.

Как это сделать...

Давайте рассмотрим следующие шаги:

находясь в Dashboard, откройте закладку Web:

Рисунок 7-7

PythonAnywhere: просмотр веб приложения

Данный интерфейс сообщает нам что у нас нет ещё пока веб приложений. Выбрав Add a new web app, мы откроем следующее окно просмотра. Оно сообщит нам, что наши приложения будут иметь следующий веб адрес: loginname.pythonanywhere.com (для данного примера веб адресом приложения выступает giazax.pythonanywhere.com):

Рисунок 7-8

PythonAnywhere: мастер веб приложения

Когда мы кливкаем по Next, мы можем выбрать веб инфраструктуру Python, которую мы бы желали использовать:

Рисунок 7-9

PythonAnywhere: мастер веб инфраструктуры

Мы выбираем в качестве веб инфраструктуры Flask, а затем жмём на Next чтобы выбрать ту версию Python, которой мы бы хотели воспользоваться здесь:

Рисунок 7-10

PythonAnywhere: мастер веб инфраструктуры

Flask является микро- инфраструктурой для Python, которая проста в установке и применении и используется такими компаниями как Pinterest и LinkedIn.

[Замечание]
Замечание

Если вы желаете знать что представляет собой инфраструктура для создания веб- приложений, вы можете представить себе некий набор программ с целью облегчения разработки веб- служб, таких как веб серверы и интерфейсы API. Более подробные сведения относительно Flask можно найти по следующей ссылке.

В своём предыдущем снимке экрана мы выбрали Python 3.5 для Flask 1.0.2, а затем кликнули по Next чтобы войти в необходимый путь файла Python чтобы применять его для размещения нашего приложения Flask. Здесь выбран устанавливаемый по умолчанию файл:

Рисунок 7-11

PythonAnywhere: определение проекта Flask

Когда мы кликаем по Next в последний раз, отображается следующий экран, который суммирует параметры настройки для нашего веб приложения:

Рисунок 7-12

PythonAnywhere: страница настроек для giazax.pythonanywhere.com

Теперь мы рассмотрим что у нас получилось.

Как это работает...

В адресной полосе своего веб браузера наберите URL нашего веб приложения, в нашем случае https://giazax.pythonanywhere.com/. Этот сайт покажет простую фразу приветствия:

Рисунок 7-13

Страница сайта giazax.pythonanywhere.com

Наш исходный код для данного приложения можно увидеть выбрав Go to directory в соответствии со значением метки Source code:

Рисунок 7-14

PythonAnywhere: страница настроек

Здесь можно проанализировать те файлы, которые мы сделали для своего веб приложения:

Рисунок 7-15

PythonAnywhere: репозиторий сайта проекта

Также имеется возможность загрузки новых файлов и возможность изменения содержимого. В данном случае мы выбираем flask_app.py в качестве своего первого веб приложения. Его содержимое выглядит как минимальное приложение Flask:

# A very simple Flask Hello World app for you to get started with...

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello from Flask!'

Декоратор route() применяется Flask для задания значения URL, которое должно включать нашу функцию hello_world. Эта простейшая функция возвращает данное отображаемое в нашем веб браузере сообщение.

Также ознакомьтесь...

Сам PythonAnywhere выполнен на HTML, что делает его переносимым по множеству платформ и браузеров, включая мобильные версии Apple. Имеется возможность удерживать открытыми определённые оболочки (с различным числом в соответствии с выбранным профилем учётной записи), разделять их с прочими пользователями или прекращать их в случае необходимости.

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

Каждая оболочка может содержать сценарий WSGI, который соответствует конкретному URL. Также имеется возможность запуска оболочки bash, в которой вы можете вызывать Git и взаимодействовать с его файловой системой. Наконец, как мы можем видеть, имеется некий мастер, который позволяет нам предварительно настраивать приложение Django, web2py или Flask.

Более того, имеется возможность эксплуатации базы данных MySQL, что является последовательностью заданий cron, которые позволяют нам периодически исполнять определённые сценарии. Следовательно, мы получим истинную сущность PythonAnywhere: разработку веб приложения со скоростью света.

PythonAnywhere целиком полагается на инфраструктуру Amazon EC2, поэтому нет никаких причин не доверять данной службе. Поэтому она настоятельно рекомендуется для тех, кто задумывается о персональном использовании. Обсуждавшаяся учётная запись начинающего предлагает больше ресурсов, чем соответствующие в Heroku, развёртывание проще чем для OpenShift, а получаемая целиком система, как правило, гораздо более гибкая чем Google App Engine.

Дополнительно

Основные ресурсы для PythonAnywhere можно найти по ссылке.

Для веб программирования через Python PythonAnywhere также поддерживает Django и web2py, помимо Flask.

Что касается Flask, вам рекомендуется посетить эти площадки для получения сведений как работать с этими библиотеками.

Докеризация приложения Python

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

Это делает контейнеры крайне привлекательной технологией в плане масштабируемости, производительности и изоляции. Контейнеры не являются юной технологией; они были с успехом запущены Docker в 2013. {Прим. пер.: не стоит также забывать и наших соотечественников Virtuozzo, Parallels, aka SWSoft, одними из первых представившими контейнеры ещё в первой половине 2000-х.} Начиная с этого момента они полностью перевернули все стандарты, применяемые для разработки приложений и управления ими. {Прим. пер.: подробнее о докеризации на примере Rails.}

Докер является контейнерной платформой, основанной на реализации LXC (Linux Containers) и которая расширяет данную технологию возможностью управления контейнерами как самостоятельно наполняемыми образами, а также добавила инструменты для координации их жизненного цикла и сохранения их состояния. {Прим. пер.: так в тексте. На самом деле, начиная с версии 0.9 Docker содержит собственный компонент, libcontainer для непосредственного управления предлагаемых ядром Linux средств виртуализации (cgroup), дополнительно к применению интерфейсов абстракции виртуализации libvirt, LXC и systemd-nspawn.}

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

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

Теперь давайте рассмотрим как осуществить докеризацию приложения Python с применением Docker.

Приготовление

Основное чутьё команды Docker состояло в том, чтобы взять свою концепцию контейнера и выстроить некую экосистему вокруг неё с тем, чтобы упростить его применение. Эта экосистема содержит некую последовательность инструментов:

Механизм Docker

Механизм Docker

Swarm

Kitematic

Установка Docker for Windows

Вся установка достаточно проста: после того как вы выгрузите необходимый установщик, просто запустите его и это всё что вам требуется. Данный процесс установки в целом очень прямолинеен. Единственный момент, который требует внимания, так это завершающая стадия установки, которая может потребовать включения функциональности Hyper-V. Если это так, мы примем это и перезапустим свою машину. {Прим. пер.: Windows Server 2019 содержит лицензию на Docker Enterprise, который может быть вытащен и запущен без необходимости посещения Docker Hub, см. наш перевод 2 издания Полного руководства Windows Server 2019 Джордана Краузе. Кроме того, Windows Server 2019 имеет поддержку MobyVM и LCOW, которые позволяют контейнерам Linux работать на хосте контейнеров Windows Server, даже работая бок о бок с контейнерами Windows!}

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

Откройте приглашение командной строки или консоль PowerShell и убедитесь что всё хорошо, выполнив команду docker version:

C:\> docker version
Client: Docker Engine - Community
 Version: 18.09.2
 API version: 1.39
 Go version: go1.10.8
 Git commit: 6247962
 Built: Sun Feb 10 04:12:31 2019
 OS/Arch: windows/amd64
 Experimental: false

Server: Docker Engine - Community
 Engine:
  Version: 18.09.2
  API version: 1.39 (minimum version 1.12)
  Go version: go1.10.6
  Git commit: 6247962
  Built: Sun Feb 10 04:13:06 2019
  OS/Arch: linux/amd64
  Experimental: false

Наиболее интересной частью в этом выводе является подраздел, который делается между самим клиентом и его сервером. Таким клиентом является наша локальная система Windows, в то время как сервером выступает та виртуальная машина Linux, которую Docker установил за сценой. Эти части взаимодействуют друг с другом благодаря уровню имеющемуся API, как это уже упоминалось во введении в данный рецепт.

Теперь давайте рассмотрим как контейнеризовать (докеризовать) простое приложение Python.

Как это сделать...

Давайте предположим, что мы хотим развернуть следующее приложение Python, которое мы назовём dockerize.py:

from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
    return "Hello World!"
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int("5000"), debug=True)

Данный пример приложения применяет уже знакомый нам модуль Flask. Он реализует некое простейшее веб приложение с локальным адресом, {по порту} 5000.

Самый первый шаг состоит в создании следующего текстового файла с расширением .py, который мы обзовём Dockerfile.py:

FROM python:alpine3.7
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE 5000
CMD python ./dockerize.py

Перечисленный в предыдущем примере директивы выполняют следующие задачи:

FROM python: alpine3.7: указывает Docker на необходимость использования Python с версией 3.7.

COPY копирует наше приложение в имеющийся образ контейнера.

WORKDIR устанавливает значение рабочего каталога (WORKDIR).

Инструкция RUN вызывает установщик pip, указывая для него файл requirements.txt. Он содержит список зависимостей, которые обязано использовать данное приложение (в нашем случае это flask).

Директива EXPOSE выставляет значение того порта, который применяет flask.

Итак, в сумме мы написали три файла:

Подлежащее упаковке в контейнер приложение: dockerize.py

Dockerfile

Файл списка зависимостей

В итоге нам требуется создать некий образ для своего приложения dockerize.py:

docker build --tag dockerize.py

Это пометит тегом мой образ my-python-app и построит его.

Как это работает...

После того как построено приложение my-python-app, мы можем запустить его в качестве контейнера:

docker run -p 5000:5000 dockerize.py

Далее наше приложение запускается в виде контейнера, после чего соответствующий параметр названия отсылает значение названия этого контейнера, а параметр -p устанавливает соответствие значения порта хоста 5000 порту этого контейнера 5000.

Затем нам следует открыть свой веб браузер, перейти в полосу адреса и набрать localhost:5000. Если все отработало правильно, тогда вы должны обнаружить следующую веб страницу:

Рисунок 7-16

Приложение Docker

Docker запустил контейнер dockerize.py при помощи команды run и результатом этого получено некое веб приложение. Наш образ содержит все инструкции, которые требуются для работы с этим контейнером.

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

Будет полезным кратко повторить то что произошло при создании некого экземпляра контейнера:

Необходимый образ нашего контейнера (если он ещё не присутствует) загружается локально.

Создаётся некая среда, в которой запускается данный контейнер.

На ваш экран выводится сообщение.

Предварительно созданная среда затем остаётся брошенной.

Всё это происходит в несколько секунд, причём в некой простой, интуитивно ясной и доступной для чтения команде.

Также ознакомьтесь...

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

Возможная пригодная для контейнеров программная архитектура является классической архитектурой микрослужб. Основная идея состоит в разбиении своего приложения на множество небольших компонентов - каждое для соей собственной задачи - которые способны обмениваться сообщениями и сотрудничать друг с другом. Развёртывание таких компонентов затем будет происходить индивидуально, причём в виде множества контейнеров.

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

Рисунок 7-17

Архитектура микрослужб в реализации виртуальной машины и Docker

В виртуальных машинах некий инструмент с названием Гипаервизор заботится о резервировании (статическом или динамическом) определённого объёма ресурсов из ОС хоста для выделения одной или более ОС, имеющих название гостевых или размещаемых (hosts). Гостевая ОС будет целиком изолирована от ОС самого хоста. Такой механизм чрезвычайно затратен в плане ресурсов, а потому мысли сочетания микрослужб с какими- то виртуальными машинами совершенно невозможна.

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

Когда мы просим Docker запустить некий контейнер из его образа, он должен быть представлен на имеющемся локальном диске, в противном случае Docker выдаст нам сообщение о возникшей проблеме (с содержимым, которое читается как Unable to find image 'hello-world: latest' locally - не способен обнаружить локально 'hello-world: latest''hello-world: latest') и будет его выгружать в автономном режиме.

Чтобы отыскать какие образы были выгружены из Docker в наш компьютер, мы пользуемся командой docker images:

C:\> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dockerize.py latest bc3d70b05ed4 23 hours ago 91.8MB
<none> <none> ca18efb44b3c 24 hours ago 91.8MB
python alpine3.7 00be2573e9f7 2 months ago 81.3MB

Данный репозиторий это контейнер относящихся к нему образов. Например, репозиторий dockerize содержит различные версии докеризованных образов. В мире Docker термин tag является более правильным для применения выражения данного понятия сопровождения версий образов. В нашем предыдущем примере, данный образ был помечен тегом в качестве самого последнего и при этом единственного тега, доступного для репозитория dockerize.

Самый последний (latest) тег является устанавливаемым по умолчанию тегом: всякий раз когда мы ссылаемся на некий репозиторий без определения названия его тега, Docker будет подразумевать ссылку на на этот тег latest, а когда он отсутствует, будет отображаться некая ошибка. Таким образом, в качестве рекомендации, форма со значением тега репозитория будет более предпочтительной, так как она позволяет лучше предсказывать соответствующее содержимое данного образа, избегая возможных конфликтов между контейнерами и ошибок по причине отсутствия тега latest.

Дополнительно

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

{Прим. пер.: по нашему мнению великолепным образчиком для уяснения самой технологии разработки приложений на основе Docker может служить наш перевод Docker для разработчиков Rails Роб Айзенберг, в которой Rails запросто может быть заменён пытливым умом на любую иную инфраструктуру разработки - например, TensorFlow. Для тех, кто интересуется управлением очень больших количеств контейнеров, в том числе с автоматической балансировкой нагрузки, высокой доступностью и прочим, рекомендуем знакомство с перевод основных глав из Полного руководства Kubernetes Джиджи Сэйфана (2е издание).}

В своём следующем разделе мы изучим основные свойства безсерверных вычислений, основная цель которых состоит в том чтобы упростить для разработчиков программного обеспечения составление разрабатываемого кода для его запуска в некой облачной платформе.

Введение в вычисления без серверов

В последние годы была разработана новая модель служб с названием FaaS (Function as a Service), которая также именуется как безсерверные вычисления (serverless computing).

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

Самая первая модель относится к тем дням, когда Amazon реализовала свою службу AWS Lambda в 2014. Со временем дополнительно к решению Anazon появились прочие альтернативы, которые были разработаны основными производителями, такими как Microsoft с их Azure Functions, а также IBM и Google с их собственными Cloud Functions. Также существуют допустимые решения с открытым исходным кодом: из наиболее широко применяемых у нас имеются Apache OpenWhisk, которая используется IBM в их Bluemix для своего безсерверного предложения, а также OpenLambda и IronFunctions, причём последняя основывается на контейнерной технологии Docker.

В данном рецепте мы рассмотрим как реализовать безсерверную функцию Python через span class="term">AWS Lambda.

Приготовление

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

Данный вид служб является платным. Однако на самый первый год доступен некий свободный слой. Это некий набор служб, использующих минимум ресурсов и его можно бесплатно применять для целей оценки таких служб, а также для разработки приложений.

[Замечание]
Замечание

Для получения дополнительных сведений о том, как создать некую бесплатную учётную запись в AWS, будьте любезны ознакомиться с официальной документацией Amazon.

В этом разделе мы обозначим основы запуска кода в AWS Labmda без необходимости предоставления каких бы то ни было серверов или управления ими. Мы покажем создание в Лямбда функции Hello World с применением консоли AWS Lambda. Мы также поясним как вручную вызывать функцию Лямбда применяя простые сведения о событии и как интерпретировать получаемые на выходе параметры. Все показываемые нами в данном руководстве действия могут быть выполнены как часть подобного свободного плана в https://aws.amazon.com/free.

Как это сделать...

Мы рассмотрим следующие шаги:

Прежде всего мы зарегистрируемся в консоли Лямбда. Затем вам потребуется определиться с местоположением и выбрать Lambda в вычислениях чтобы открыть необходимую консоль AWS Lambda (которая выделена зелёным на следующем снимке экрана):

Рисунок 7-18

AWS: Выбор службы Lambda

Затем в жтой консоли AWS Lambda выберите Get Started Now и вслед за этим создайте некую функцию Лямбда:

Рисунок 7-19

AWS: Стартовая страница Lambda

В своём блоке фильтрации наберите hello-world-python и выберите копирку hello-world-python.

Теперь требуется настроить нашу функцию Лямбда. Наш следующий список показывает настройки и предоставляет образцы значений:

Функции настройки:

Name: Введите здесь название для своей функции. В данном руководстве мы применяем hello-world-python.

Description: Здесь вы можете вводить краткое описание данной функции. Этот блок имеет предварительную установку с фразой A starter AWS Lambda Function.

Runtime: На данный момент имеется возможность написанияя необходимого кода Лямбда функции в Java, Node.js, а также Python 2.7, 3.6 и 3.7. Для целей данного руководства мы настраиваем средой исполнения Python 2.7.

Код лямбда функции:

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

Обработчик и роль функции Лямбда:

Handler: Вы можете определить некий метод, в котором AWS Lambda может запускать на исполнение данный код. AWS Lambda предоставляет данные событий в качестве входа того обработчика, который будет обрабатывать эти события. В нашем примере Лямбда указывает на события из рассматриваемого примера кода, а потому данное поле будет компилироваться с lambda_function.lambda_handler.

Role: Кликните по ниспадающему меню и выберите Basic Execution Role:

Рисунок 7-20

AWS: Страница настройки функции

На данный момент необходимо создать некую роль для исполнения (имеющую название IAM Role) с необходимой авторизацией для интерпретации AWS Lambda в качестве исполнителя данной Лямбда функции. Кликнув по Allow, вы получите результатом страницу Configure function и будет выбрана функция lambda_basic_execution:

Рисунок 7-21

AWS: Итоговая страница роли

Данная консоль сохраняет весь код в сжатом файле, что представляется в его пакете распространения. Эта консоль далее загружает полученный пакет распространения в AWS Lambda для создания необходимой функции Лямбла:

Рисунок 7-22

AWS: Страница обзора Lambda

Теперь у нас имеется возможность проверки этой функции, сверки результатов и отображения регистрационных записей:

для запуска самой первой Лямбда функции, кликните по Test:

Рисунок 7-23

AWS: Страница проверок Lambda

Во всплывающем редакторе введите некое событие для проверки своей функции.

Выберите из перечня Sample event template Hello World в соответствующей странице Input test event:

Рисунок 7-24

AWS: Шаблон Lambda

Кликните по Save and test. Затем AWS Lambda вцыполнит данную функцию от вашего имени.

Как это работает...

Когда выполнение завершится, имеется возможность просмотреть в своей консоли полученные результаты:

Раздел Execution result документирует правильность выполнения данной функции.

Раздел Summary показывает наиболее важные сведения из отчёта в разделе Log output.

Раздел Log output отображает все зарегистрированные записи исполнения вашей функции Лямбда:

Рисунок 7-25

AWS: Результаты выполнения

Также ознакомьтесь...

AWS Lambda отслеживает все функции и автоматически вырабатывает параметры отчётов через Amazon CloudWatch (смотрите на приводимом ниже снимке экрана). Для упрощения мониторинга вашего кода в процессе его исполнения, AWS Lambda автоматически отслеживает общее число запросов, значение задержки для каждого запроса, а также общее число запросов с ошибками, которые публикуются со связанными с ними параметрами:

Рисунок 7-26

Что такое функция Лямбда?

Функция Лямбда содержит код, который разработчик желает выполнить в ответ на некое событие. Сам разработчик заботится о настройках данного кода и определении необходимых требований в плане ресурсов внутри своей предоставляемой поставщиком консоли. Всё остальное, включая определение размера ресурсов, автоматически управляется поставщиком услуги на основе требований рабочей нагрузки.

Почему без сервера?

Преимущества безсерверного решения следующие:

Отсутствие управления инфраструктурой: Разработчики могут сосредоточиться на самом выстраиваемом продукте вместо того чтобы работать с серверами времени исполнения и управлять ими.

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

Оптимизация использования ресурсов: Так как сама обработка и ресурсы хранения выделяются автоматически, больше нет необходимости в дальнейшем инвестиций в лишние мощности.

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

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

Улучшение выхода на рынок: Исключение затрат на управление инфраструктурой позволяет разработчикам сосредоточиться на качестве продукта и быстрее привносит свой код в промышленное использование.

Возможные проблемы и ограничения

Существуют некие обманки, которые следует принимать во внимание при оценке приемлемости безсерверных вычислений:

Возможность утраты производительности: Когда ваш код не используется очень часто, тогда могут возникать некие проблемы с задержками в его исполнении. Они важны по сравнению с теми случаями, кода он постоянно исполняется в сервере, виртуальной машине или контейнере. Это происходит по той причине, что (в отличие от того что происходит при применении политик автоматического масштабирования) для модели без серверов поставщик облачного решения полностью высвобождает ресурсы в случае когда данный код не используется. Это подразумевает, что когда запуск требует какого- то времени, то на начальном этапе неизбежно появляется дополнительная задержка.

Режим без сохранения состояния: Безсерверные функции работают в режиме без сохранения состояния. Это означает, что когда вы желаете добавить логику сохранения неких элементов, таких как параметры для передачи аргументов в некую другую функцию, тогда вам требуется добавлять компонент какого- то постоянного хранилища в поток вашего приложения и соединять эти события друг с другом. Например, Amazon предоставляет некий дополнительный инструмент с названием AWS Step Functions, который координирует значения состояния и управляет ими для всех микрослужб и распределённых компонентов безсерверных приложений.

Ограничения на ресурсы: Безсерверные вычисления не годятся для некоторых видов рабочих нагрузок или вариантов использования, в частности, для высокопроизводительных вычислений и для ограничений в применении ресурсов, которые предоставляются поставщиком облачного решения (к примеру, AWS ограничивает общее число одновременно запущенных Лямбда функций). Это обусловлено как самой сложностью в предоставлении такого числа желательных серверов в неком размере, так и фиксацией периода времени.

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

Дополнительно

Как мы уже отмечали, отправной точкой для работы с безсерверными архитектурами является сама инфраструктура (AWS). По предыдущему URL вы сможете отыскать множество сведений и руководств, включая описанный в данном разделе пример. {Прим. пер.: также обращаем внимание на перевод кое- каких глав (2-5) из Kubernetes для приложений без сервера Русса МакКендрика.}

Глава 2. Параллельность на основе потоков

Содержание

Глава 2. Параллельность на основе потоков

Что такое поток?

Модуль Python threading

Определение потока

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Выявление значения текущего потока

Приготовление

Как это сделать...

Как это работает...

Задание подкласса потока

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков блокировкой

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков RLock

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков при помощи семафоров

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков условием

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Синхронизация потоков событием

Приготовление

Как это сделать...

Как это работает...

Синхронизация потоков барьером

Приготовление

Как это сделать...

Как это работает...

Синхронизация потоков очередью

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

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

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

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

Мы вначале определим сами понятия основы потока и многопосточного программирования, а затем сделаем введение в соответствующую библиотеку multithreading. Мы изучим основные руководства для определения потоков, их управления и взаимодействия.

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

В этой главе мы рассмотрим такие рецепты:

Что такое поток?

Как задать поток

Как определить текущий поток

Как применять поток в неком подклассе

Синхронизация потоков блокировкой

Синхронизация потоков через RLock

Синхронизация потоков семафорами

Синхронизация потоков посредством условия

Синхронизация потоков событием

Синхронизация потоков барьером

Синхронизация потоков очередью

Мы также исследуем основные варианты, предлагаемые со стороны Python для программирования при помощи потоков. Для этого мы сосредоточимся на использовании метода threading.

Что такое поток?

Некий поток (thread) это независимое течение (flow) которое может исполняться параллельно и одновременно с прочими потоками в системе.

Множество потоков могут совместно использовать данные и ресурсы, получая преимущества так называемого пространства разделяемых сведений. Сами особенности потоков и процессов зависят от той ОС, в которой вы планируете запускать своё приложение, однако, в целом, можно постулировать, что некий поток содержится внутри какого- то процесса и что различные потоки при одних и тех же условиях процесса совместно разделяют некоторые ресурсы. В противоположность этому, различные процессы не разделяют свои собственные ресурсы с прочими процессами.

Некий поток составляется из трёх элементов: программных счётчиков, регистров и стека. Совместные с прочими потоками ресурсы в том же самом процессе по существу содержат данные и ресурсы ОС. Более того, потоки обладают своим собственным состоянием исполнения, а именно, состоянием потока, а также могут выполнять синхронизацию с другими потоками.

Состоянием потока могут быть готов (ready), исполняется (running) и блокирован (blocked):

При создании некого потока он входит в состояние Готов.

Поток планируется к выполнению своей ОС (или системой сопровождения времени исполнения) и, когда появляется его включение, он начинает своё выполнение, переходя в состояние Исполняется.

Конкретный поток может ожидать исполнения некого условия, переходя из состояния Исполняется в состояние Блокирован. Когда такое блокирующее условие прекращается, Блокированный поток возвращается в состояние Готов.

Рисунок 2-1

Жизненный цикл потока

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

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

Модуль Python threading

Python управляет потоками при помощи модуля threading, предоставляемого стандартной библиотекой Python. Этот модуль предоставляет некоторые очень интересные свойства, которые делают намного более простым подход на основании потоков; фактически, сам модуль threading предоставляет несколько механизмов синхронизации, которые очень просты в реализации.

Основными составляющими модуля threading являются:

Объект thread

Объект lock

Объект RLock

Объект semaphore

Объект condition

Объект event

В своих последующих рецептах мы изучим основные свойства, предлагаемые самой библиотекой threading на различных примерах приложений. Во всех идущих далее примерах мы будем следовать дистрибутиву Python 3.5.0/

Определение потока

Самый простой способ применения некого потока состоит в создании его экземпляра с некой целевой функцией и последующим вызовом метода запуска чтобы позволить ему начать своё задание.

Приготовление

Модуль Python threading предоставляет некий класс Thread, который используется для запуска процессов и функций в различных потоках:

class threading.Thread(group=None,
                       target=None,
                       name=None,
                       args=(),
                       kwargs={})

Вот значения параметров самого класса Thread:

group: Это именно то значение group, которое должно быть установлено в None; оно зарезервировано для последующих реализаций.

target: Это значение той функции, котороая должна быть выполнена при запуске вами некого действия потока.

name: Является названием данного потока; по умолчанию ему назначается некое уникальное имя в виде Thread-N.

args: Это определённый кортеж аргументов, которые должны быть переданы в target.

kwargs: Это заданный словарь аргументов с ключевыми словами, который должен применяться к установленной функции target.

В своём следующем разделе мы изучим как задавать некий поток.

Как это сделать...

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

Импортируем модуль threading при помощи следующей команды Python:

import threading

В нашей программе main создаётся некий экземпляр объекта Thread с функцией target под названием my_func. Далее будет включён некий аргумент для этой функции, передаваемый в получаемом сообщении вывода:

t = threading.Thread(target=my_func, args=(i,))

Данный поток не приступит к исполнению, пока не будет вызван метод start, а метод join создаст вызывающий поток и будет ожидать пока этот поток не завершит своё исполнение следующим образом:

import threading

def my_func(thread_number):
    return print('my_func called by thread N°\
        {}'.format(thread_number))

def main():
    threads = []
    for i in range(10):
        t = threading.Thread(target=my_func, args=(i,))
        threads.append(t)
        t.start()
        t.join()

if __name__ == "__main__":
    main()

Как это работает...

В своей программе main мы инициализируем необходимый список потоков, в который мы добавляем соответствующий экземпляр каждого создаваемого потока. Общее число созданных потоков равно 10, в то время как значения индекса i для iго потока передаётся в качестве некого аргумента в сам iй поток:

my_func called by thread N°0
my_func called by thread N°1
my_func called by thread N°2
my_func called by thread N°3
my_func called by thread N°4
my_func called by thread N°5
my_func called by thread N°6
my_func called by thread N°7
my_func called by thread N°8
my_func called by thread N°9

Также ознакомьтесь...

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

Выявление значения текущего потока

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

Именованные потоки полезны в серверных процессах со множеством обслуживающих потоков, которые обрабатывают различные операции.

Приготовление

Данный модуль threading предоставляет метод currentThread().getName(), который возвращает значение названия данного текущего потока.

Наш последующий раздел показывает нам как применять эту функцию для определения какой поток запущен.

Как это сделать...

Давайте взглянем на следующие этапы:

Для определения того, какой именно поток исполняются, мы создадим три функции target и импортируем необходимый модуль time чтобы ввести некое подвешивание исполнения на две секунды:

import threading
import time

def function_A():
    print (threading.currentThread().getName()+str('-->\
        starting \n'))
    time.sleep(2)
    print (threading.currentThread().getName()+str( '-->\
        exiting \n'))

def function_B():
    print (threading.currentThread().getName()+str('-->\
        starting \n'))
    time.sleep(2)
    print (threading.currentThread().getName()+str( '-->\
        exiting \n'))

def function_C():
    print (threading.currentThread().getName()+str('-->\
        starting \n'))
    time.sleep(2)
    print (threading.currentThread().getName()+str( '-->\
        exiting \n'))

Экземпляры трёх потоков создаются при помощи функции target. Затем мы передаём то название которое следует вывести на печать и, если оно не определено, тогда будет использоваться установленное по умолчанию название. Далее вызываются методы start() и join() для каждого потока:

if __name__ == "__main__":

    t1 = threading.Thread(name='function_A', target=function_A)
    t2 = threading.Thread(name='function_B', target=function_B)
    t3 = threading.Thread(name='function_C',target=function_C)

    t1.start()
    t2.start()
    t3.start()

    t1.join()
    t2.join()
    t3.join()

Как это работает...

Мы намерены установить три потока, причём каждый из них назначается функцией target. Когда эта функция target выполнена и завершена, само название функции выводится на печать надлежащим образом.

Для данного примера вывод будет выглядеть как- то так (даже хотя сам показанный порядок может быть не в точности тем же):

function_A--> starting
function_B--> starting
function_C--> starting

function_A--> exiting
function_B--> exiting
function_C--> exiting

Задание подкласса потока

Создание некого потока может потребовать определения какого- то подкласса, который наследуется из самого класса Thread. Последний, как пояснялось в разделе Определение потока, входит в состав модуля threading, который следует импортировать.

Приготовление

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

Как это сделать...

Необходимые шаги содержат следующее:

Мы определяем необходимый класс MyThreadClass, который мы можем применять для создания всех желаемых нами потоков. Каждый поток с этим типом будет характеризоваться теми операциями, которые определены в методе run, который, в данном простом примере, ограничивает себя выводом на печать какой-то строки в самом начале и по завершению своего выполнения:

import time
import os
from random import randint
from threading import Thread

class MyThreadClass (Thread):

Более того, в сомом методе __init__ нам необходимо задать два параметра инициализации, соответственно, name и duration, которые будут использованы в методе run:

def __init__(self, name, duration):
      Thread.__init__(self)
      self.name = name
      self.duration = duration

   def run(self):
      print ("---> " + self.name +\
             " running, belonging to process ID "\
             + str(os.getpid()) + "\n")
      time.sleep(self.duration)
      print ("---> " + self.name + " over\n")

Эти параметры далее будут настроены в процессе самого создания данного потока. В частности, значение параметра duration вычисляется на основании функции randint, которая выводит некое случайное целое от 1 до 10. Начиная с собственно определения MyThreadClass, давайте рассмотрим как создать экземпляры дополнительных потоков следующим образом:

def main():

    start_time = time.time()

    # Создание потока
    thread1 = MyThreadClass("Thread#1 ", randint(1,10))
    thread2 = MyThreadClass("Thread#2 ", randint(1,10))
    thread3 = MyThreadClass("Thread#3 ", randint(1,10))
    thread4 = MyThreadClass("Thread#4 ", randint(1,10))
    thread5 = MyThreadClass("Thread#5 ", randint(1,10))
    thread6 = MyThreadClass("Thread#6 ", randint(1,10))
    thread7 = MyThreadClass("Thread#7 ", randint(1,10))
    thread8 = MyThreadClass("Thread#8 ", randint(1,10))
    thread9 = MyThreadClass("Thread#9 ", randint(1,10))

    # Запуск потока
    thread1.start()
    thread2.start()
    thread3.start()
    thread4.start()
    thread5.start()
    thread6.start()
    thread7.start()
    thread8.start()
    thread9.start()

    # Присоединение потока
    thread1.join()
    thread2.join()
    thread3.join()
    thread4.join()
    thread5.join()
    thread6.join()
    thread7.join()
    thread8.join()
    thread9.join()

    # Кнец
    print("End")

    #Время исполнения
    print("--- %s seconds ---" % (time.time() - start_time))

if __name__ == "__main__":
    main()

Как это работает...

В этом примере мы создали девять потоков, причём каждый со своим собственным name свойством duration, в соответствии со сделанным определением в его методе __init__.

Далее мы запустили из при помощи метода start, который ограничен исполнением самого содержимого ранее заданного метода run. Обратите внимание, что идентификатор процесса для каждого потока тот же самый, что означает, что мы пребываем в неком процессе со множеством потоков.

Кроме того, обратите внимание, что сам метод запуска не блокируется: при его выполнении, само управление немедленно переходит к следующей строке, в то время как сам поток стартует в фоновом режиме. На самом деле, как вы можете видеть, собственно создание потоков не происходит в том порядке, который определён в этом коде. Подобным образом, прекращение потока вынуждается тем значением, которое определено значением параметра duration, вычисляемым на основании функции randint, а также передаваемым в качестве соответствующего параметра для каждого из создаваемых экземпляров потока. Для того чтобы дождаться завершения некого потока, следует выполнить операцию join.

Получаемый вывод выглядит подобным образом:

--->  Thread#1 running, belonging to process ID 13084
--->  Thread#5 running, belonging to process ID 13084
--->  Thread#2 running, belonging to process ID 13084
--->  Thread#6 running, belonging to process ID 13084
--->  Thread#7 running, belonging to process ID 13084
--->  Thread#3 running, belonging to process ID 13084
--->  Thread#4 running, belonging to process ID 13084
--->  Thread#8 running, belonging to process ID 13084
--->  Thread#9 running, belonging to process ID 13084

--->  Thread#6 over
--->  Thread#9 over
--->  Thread#5 over
--->  Thread#2 over
--->  Thread#7 over
--->  Thread#4 over
--->  Thread#3 over
--->  Thread#8 over
--->  Thread#1 over

End

--- 9.117518663406372 seconds ---

Также ознакомьтесь...

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

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

Однако (исключительно чтобы насторожит читателя!), наследование способно сделать более сложным прочтение такой программы. эТо обусловлено тем, что при вызове некого метода не всегда отчётливо понятно где это было определено внутри самого кода, что требуется отслеживать внутри множества методов, вместо того, чтобы размещаться в едином чётко определённом месте.

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

Синхронизация потоков блокировкой

Модуль threading также содержит некий простейший механизм блокировки, который позволяет нам реализовывать синхронизацию между потоками.

Приготовление

Некая блокировка ничто иное, как какой- то объект, который обычно доступен множеству потоков и которым эти потоки обязаны располагать прежде чем они смогут продолжить исполнение некого защищённого раздела некой программы. Такие блокировки создаются посредством исполнения имеющегося метода Lock(), который определяется в самом деле threading.

После того, как необходимая блокировка создана, мы можем применять два метода, которые делают возможной синхронизацию самого выполнения двух (или более) потоков: метод acquire() для овладения управления этой блокировкой и метод release() для её высвобождения.

Метод acquire() получает некий необязательный параметр , который, когда он не задан ил установлен в значение True, принуждает данный поток подвешивать своё исполнение пока данная блокировка на высвободится и появится возможность овладения ею. Когда, в противном случае, такой метод acquire() исполняется с неким аргументом, равным False, тогда он немедленно возвращает Булев результат, который установлен в True, если эта блокировка захвачена или False в противном случае.

В своём следующем примере мы показываем этот механизм блокирования через видоизменение того кода, который был введён в нашем предыдущем рецепте Задание подкласса потока.

Как это сделать...

Необходимые шаги таковы:

Как это отображено в последующем блоке кода, наш класс MyThreadClass был видоизменён введением методов acquire() и release() внутри уже имевшегося метода run, в о время как определение Lock() находится вне самого определения данного класса:

import threading
import time
import os
from threading import Thread
from random import randint

# Определение блокировки
threadLock = threading.Lock()

class MyThreadClass (Thread):
   def __init__(self, name, duration):
      Thread.__init__(self)
      self.name = name
      self.duration = duration
   def run(self):
      #Запрос на блокировку
      threadLock.acquire()
      print ("---> " + self.name + \
             " running, belonging to process ID "\
             + str(os.getpid()) + "\n")
      time.sleep(self.duration)
      print ("---> " + self.name + " over\n")
      #Высвобождение установленной блокировки
      threadLock.release()

Наша функция main() не изменилась по соотношению к предыдущему образцу кода:

def main():
    start_time = time.time()
    # Thread Creation
    thread1 = MyThreadClass("Thread#1 ", randint(1,10))
    thread2 = MyThreadClass("Thread#2 ", randint(1,10))
    thread3 = MyThreadClass("Thread#3 ", randint(1,10))
    thread4 = MyThreadClass("Thread#4 ", randint(1,10))
    thread5 = MyThreadClass("Thread#5 ", randint(1,10))
    thread6 = MyThreadClass("Thread#6 ", randint(1,10))
    thread7 = MyThreadClass("Thread#7 ", randint(1,10))
    thread8 = MyThreadClass("Thread#8 ", randint(1,10))
    thread9 = MyThreadClass("Thread#9 ", randint(1,10))

    # Запуск потоков
    thread1.start()
    thread2.start()
    thread3.start()
    thread4.start()
    thread5.start()
    thread6.start()
    thread7.start()
    thread8.start()
    thread9.start()

    # Присоединение потоков
    thread1.join()
    thread2.join()
    thread3.join()
    thread4.join()
    thread5.join()
    thread6.join()
    thread7.join()
    thread8.join()
    thread9.join()

    # Конец
    print("End")
    #Execution Time
    print("--- %s seconds ---" % (time.time() - start_time))

if __name__ == "__main__":
    main()

Как это работает...

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

Самый первый поток овладевает этой блокировкой и выполняет свои задачи, в то время как прочие восемь отложены (on hold). В самом конце исполнения первого потока, то есть когда выполнен метод release(), наш второй поток получает эту блокировку, а потоки с третьего по восьмой всё ещё дожидаются окончания исполнения (то есть, опять же, только вплоть до выполнения соответствующего метода release()).

Такие выполнения овладения- блокировкой и высвобождения- блокировки требуются вплоть до девятого потока, с окончательным результатом, как следствием данного механизма блокирования, причём это выполнение имеет место в неком последовательном режиме, что можно видеть из следующего вывода:

---> Thread#1 running, belonging to process ID 10632
---> Thread#1 over
---> Thread#2 running, belonging to process ID 10632
---> Thread#2 over
---> Thread#3 running, belonging to process ID 10632
---> Thread#3 over
---> Thread#4 running, belonging to process ID 10632
---> Thread#4 over
---> Thread#5 running, belonging to process ID 10632
---> Thread#5 over
---> Thread#6 running, belonging to process ID 10632
---> Thread#6 over
---> Thread#7 running, belonging to process ID 10632
---> Thread#7 over
---> Thread#8 running, belonging to process ID 10632
---> Thread#8 over
---> Thread#9 running, belonging to process ID 10632
---> Thread#9 over

End

--- 47.3672661781311 seconds ---

Также ознакомьтесь...

Сами точки вставки соответствующих методов acquire() и release() определяют всё исполнение данного кода целиком. По этой причине, очень важно чтобы вы потратили время на анализ того, какой именно поток вы желаете применять и как вы бы хотели его синхронизировать.

К примеру, мы можем изменить место вставки своего метода release() в нашем классе MyThreadClass следующим образом:

import threading
import time
import os
from threading import Thread
from random import randint

# Lock Definition
threadLock = threading.Lock()

class MyThreadClass (Thread):
   def __init__(self, name, duration):
      Thread.__init__(self)
      self.name = name
      self.duration = duration
   def run(self):
      #Acquire the Lock
      threadLock.acquire()
      print ("---> " + self.name + \
             " running, belonging to process ID "\
             + str(os.getpid()) + "\n")
      #Release the Lock in this new point
      threadLock.release()
      time.sleep(self.duration)
      print ("---> " + self.name + " over\n")

В этом случае получаемый вывод слегка изменится:

---> Thread#1 running, belonging to process ID 11228
---> Thread#2 running, belonging to process ID 11228
---> Thread#3 running, belonging to process ID 11228
---> Thread#4 running, belonging to process ID 11228
---> Thread#5 running, belonging to process ID 11228
---> Thread#6 running, belonging to process ID 11228
---> Thread#7 running, belonging to process ID 11228
---> Thread#8 running, belonging to process ID 11228
---> Thread#9 running, belonging to process ID 11228

---> Thread#2 over
---> Thread#4 over
---> Thread#6 over
---> Thread#5 over
---> Thread#1 over
---> Thread#3 over
---> Thread#9 over
---> Thread#7 over
---> Thread#8 over

End
--- 6.11468243598938 seconds ---

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

Синхронизация потоков RLock

Некая реентерабельная блокировка (блокировка с повторной входимостью), или просто RLock является примитивом синхронизации, которым можно овладевать множество раз из одного и того же потока.

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

Наш следующий пример демеонстрирует как управлять потоками через механизм RLock().

Приготовление

RLock реализуется посредством класса threading.RLock(). Он предоставляет методы acquire() и release(), которые имеют тот же синтаксис что и в классе threading.RLock().

Блокировкой RLock можно овладевать множество раз из одного и того же самого потока. Прочие потоки не будут иметь возможности овладеть этой блокировкой RLock до тех пор, пока владеющий ею поток не выполнит вызов release() для всех предыдущих вызовов acquire(), такая блокировка RLock должна высвобождаться, но только тем потоком, который овладел ею.

Как это сделать...

Необходимые шаги содержат:

Мы введём некий класс Box, который предоставляет свои методы add() и remove(), которые осуществляют доступ к методу execute() для выполнения необходимого действия, соответственно, по добавлению или удалению некого элемента. Дотсуп к такому методу execute() регулируется RLock():

import threading
import time
import random

class Box:
    def __init__(self):
        self.lock = threading.RLock()
        self.total_items = 0

    def execute(self, value):
        with self.lock:
            self.total_items += value

    def add(self):
        with self.lock:
            self.execute(1)

    def remove(self):
        with self.lock:
            self.execute(-1)

Наши следующие функции вызываются двумя имеющимися потоками. У них имеются в качестве параметров необходимый класс box и общее значение числа items на добавление или на удаление:

def adder(box, items):
    print("N° {} items to ADD \n".format(items))
    while items:
        box.add()
        time.sleep(1)
        items -= 1
        print("ADDED one item -->{} item to ADD \n".format(items))

def remover(box, items):
    print("N° {} items to REMOVE\n".format(items))
    while items:
        box.remove()
        time.sleep(1)
        items -= 1
        print("REMOVED one item -->{} item to REMOVE\
            \n".format(items))

Здесь устанавливается значение общего числа элементов на добавление или удаление из имеющегося набора box. Как вы можете видеть, эти два числа будут различными. Само исполнение завершается обоими методами adderи remover, совершающими свои задачи:

def main():
    items = 10
    box = Box()

    t1 = threading.Thread(target=adder, \
                          args=(box, random.randint(10,20)))
    t2 = threading.Thread(target=remover, \
                          args=(box, random.randint(1,10)))

    t1.start()
    t2.start()

    t1.join()
    t2.join()

if __name__ == "__main__":
    main()

Как это работает...

В нашей программе main два имеющихся потока t1 и t2 были ассоциированы с соответствующими функциями adder() и remover(). Эти функции активируются когда значение числа элементов больше нуля.

Собственно вызов RLock() обслуживается внутри самого метода __init__ из созданного класса Box:

class Box:
    def __init__(self):
        self.lock = threading.RLock()
        self.total_items = 0

Две имеющиеся функции adder() и remover() взаимодействуют со всеми элементами установленного класса Box соответственным образом и вызывают методы класса Box add() и remove().

При каждом вызове метода ресурс захватывается, а затем высвобождается при помощи того праметра lock, который установлен в соответствующем методе __init__.

Вот получаемый вывод:

N° 16 items to ADD
N° 1 items to REMOVE

ADDED one item -->15 item to ADD
REMOVED one item -->0 item to REMOVE

ADDED one item -->14 item to ADD
ADDED one item -->13 item to ADD
ADDED one item -->12 item to ADD
ADDED one item -->11 item to ADD
ADDED one item -->10 item to ADD
ADDED one item -->9 item to ADD
ADDED one item -->8 item to ADD
ADDED one item -->7 item to ADD
ADDED one item -->6 item to ADD
ADDED one item -->5 item to ADD
ADDED one item -->4 item to ADD
ADDED one item -->3 item to ADD
ADDED one item -->2 item to ADD
ADDED one item -->1 item to ADD
ADDED one item -->0 item to ADD
>>>

Также ознакомьтесь...

Отличия между lock и RLock следующие:

Блокировкой lock можно овладевать лишь единожды до того как она должна быть высвобождена. Однако блокировкой RLock можно овладевать множество раз из одного и того же потока; она должна высвобождаться в точности то же самое число раз для своего высвобождения.

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

Синхронизация потоков при помощи семафоров

Некий семафор является каким- то абстрактным типом данных, который отправляется своей ОС для синхронизации доступа со стороны множества потоков к совместным ресурсам и данным. Он состоит из некой внутренней переменной, которая указывает значение числа одновременных доступов к некому ресурсу, с которым она ассоциирована.

Приготовление

Все действия с семафорами основываются на двух функциях: acquire() и release(), как это поясняется здесь:

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

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

Понятие семафор является одним из старейших примитивов в истории информатики, введённым ранним голландским научным сотрудником- компьютерщиком Эдсгером Вибе Дейкстрой.

Наш следующий пример показывает как осуществлять синхронизацию потоков при помощи семафора.

Как это сделать...

Наш следующий код описывает некую задачу, в которой у нас имеются два потока, producer() и consumer(), которые разделяют какой- то общий ресурс, коим является соответствующий элемент. Основное задание producer() состоит в выработке такого элемента, в то время как задание потока consumer() заключается в применении того элемента, который был произведён.

Когда необходимый элемент ещё не произведён для потока consumer(), тому приходится ожидать. Как только этот элемент произведён, поток producer() уведомляет consumer(), что этим ресурсом следует воспользоваться:

Выполнив инициализацию некого семафора в значение 0, мы получаем так называемое событие семафора, единственная цель которого состоит в синхронизации вычислений двух или более потоков. В нашем случае некий поток обязан одновременно использовать данные или общие ресурсы:

semaphore = threading.Semaphore(0)

Это действие очень похоже на то, которое описан в механизме блокирования надлежащей блокировкой. Имеющийся поток producer() создаёт необходимый элемент и, после этого, он высвобождает данный ресурс, вызывая метод release():

New-VM -Name VM01 -Generation 2emaphore.release()

Аналогично, поток consumer() овладевает необходимыми данными посредством метода acquire(). Когда значение счётчика семафора равно 0, тогда это блокирует условия метода acquire() до тех пор, пока он не получит уведомление от другого потока. Когда значение семафора больше 0, он уменьшает это значение. После того как производитель создаёт некий элемент, он высвобождает этот семафор, а затем соответствующий потребитель овладевает им и потребляет этот совместный ресурс:

semaphore.acquire()

Весь процесс синхронизации, который осуществляется через описанные семафоры показан в следующем блоке кода:

import logging
import threading
import time
import random

LOG_FORMAT = '%(asctime)s %(threadName)-17s %(levelname)-8s %\
              (message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)

semaphore = threading.Semaphore(0)
item = 0

def consumer():
    logging.info('Consumer is waiting')
    semaphore.acquire()
    logging.info('Consumer notify: item number {}'.format(item))

def producer():
    global item
    time.sleep(3)
    item = random.randint(0, 1000)
    logging.info('Producer notify: item number {}'.format(item))
    semaphore.release()

#Main program
def main():
    for i in range(10):
        t1 = threading.Thread(target=consumer)
        t2 = threading.Thread(target=producer)

        t1.start()
        t2.start()

        t1.join()
        t2.join()

if __name__ == "__main__":
    main()

Как это работает...

Захваченные данные затем выводятся на печать в стандартный вывод:

print ("Consumer notify : consumed item number %s " %item)

Вот результат, полученный после 10 запусков:

2019-01-27 19:21:19,354 Thread-1 INFO Consumer is waiting
2019-01-27 19:21:22,360 Thread-2 INFO Producer notify: item number 388
2019-01-27 19:21:22,385 Thread-1 INFO Consumer notify: item number 388
2019-01-27 19:21:22,395 Thread-3 INFO Consumer is waiting
2019-01-27 19:21:25,398 Thread-4 INFO Producer notify: item number 939
2019-01-27 19:21:25,450 Thread-3 INFO Consumer notify: item number 939
2019-01-27 19:21:25,453 Thread-5 INFO Consumer is waiting
2019-01-27 19:21:28,459 Thread-6 INFO Producer notify: item number 388
2019-01-27 19:21:28,468 Thread-5 INFO Consumer notify: item number 388
2019-01-27 19:21:28,476 Thread-7 INFO Consumer is waiting
2019-01-27 19:21:31,478 Thread-8 INFO Producer notify: item number 700
2019-01-27 19:21:31,529 Thread-7 INFO Consumer notify: item number 700
2019-01-27 19:21:31,538 Thread-9 INFO Consumer is waiting
2019-01-27 19:21:34,539 Thread-10 INFO Producer notify: item number 685
2019-01-27 19:21:34,593 Thread-9 INFO Consumer notify: item number 685
2019-01-27 19:21:34,603 Thread-11 INFO Consumer is waiting
2019-01-27 19:21:37,604 Thread-12 INFO Producer notify: item number 503
2019-01-27 19:21:37,658 Thread-11 INFO Consumer notify: item number 503
2019-01-27 19:21:37,668 Thread-13 INFO Consumer is waiting
2019-01-27 19:21:40,670 Thread-14 INFO Producer notify: item number 690
2019-01-27 19:21:40,719 Thread-13 INFO Consumer notify: item number 690
2019-01-27 19:21:40,729 Thread-15 INFO Consumer is waiting
2019-01-27 19:21:43,731 Thread-16 INFO Producer notify: item number 873
2019-01-27 19:21:43,788 Thread-15 INFO Consumer notify: item number 873
2019-01-27 19:21:43,802 Thread-17 INFO Consumer is waiting
2019-01-27 19:21:46,807 Thread-18 INFO Producer notify: item number 691
2019-01-27 19:21:46,861 Thread-17 INFO Consumer notify: item number 691
2019-01-27 19:21:46,874 Thread-19 INFO Consumer is waiting
2019-01-27 19:21:49,876 Thread-20 INFO Producer notify: item number 138
2019-01-27 19:21:49,924 Thread-19 INFO Consumer notify: item number 138
>>>

Также ознакомьтесь...

Неким особым видом применения семафора является взаимное исключение (mutex). Взаимное исключение это ничто иное, как семафор с некой внутренней переменной, значение которой инициализировано 1, что делает возможным взаимное исключение в доступе к данным и ресурсам.

Семафоры всё ещё широко применяются в языках программирования с множеством потоков; тем не менее, они обладают двумя основными проблемами,которые мы должны обсудить, и они таковы:

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

Вы можете попасть в ситуацию взаимной блокировки (deadlock). Например, ситуация взаимной блокировки возникает когда наш поток t1 выполняет ожидание на семафоре s1, в то время как наш поток t2 ожидает что поток t1 выполняет ожидание в s2 и t2, а затем выполнфет ожидание в s1.

Синхронизация потоков условием

Некое условие устанавливает какое- то изменение состояния в данном приложении. Именно оно является тем механизмом синхронизации, при котором поток ожидает какое- то конкретное условие, а другой поток уведомляет что это условие произошло.

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

Приготовление

Хорошим способом иллюстрации данного механизма является повторное рассмотрение задачи производитель/ потребитель. Имеющийся класс производителей выполняет записи в некий буфер, если он не заполнен, а другой класс потребителей получает данные из этого буфера (исключая их из последнего), когда буфер заполнен. Класс производителей будет уведомлять своих потребителей что общий буфер не пуст, в то время как потребители будут отчитываться перед своими производителями что этот буфер не заполнен.

Как это сделать...

Необходимые для этого шаги таковы:

Наш класс потребителей овладевает имеющимся совместным ресурсом , который моделируется посредством списка items[]:

condition.acquire()

Когда значение длины этого списка равно 0, тогда наш потребитель помещается в состояние ожидания:

if len(items) == 0:
   condition.wait()

Далее он выполняет некую операцию pop из общего списка элементов:

items.pop()

Итак, состояние потребителя уведомляет его производителя, а совместный ресурс высвобождается:

condition.notify()

Имеющийся класс производителя овладевает общим совместным ресурсом и затем он проверяет не является ли этот список полностью заполненным(в нашем примере мы разместили значение максимального числа элементов, 10, с тем, чтобы они могли содержаться в данном списке элементов). Когда наш список заполнен, тогда наш производитель помещается в состояние ожидания до тех пор, пока не произойдёт потребление списка:

condition.acquire()
if len(items) == 10:
   condition.wait()

Когда список не заполнен, добавляется ещё один элемент. Происходит уведомление об этом состоянии и данный ресурс высвобождается:

condition.notify()
condition.release()

Для отображения механизма условия мы снова применим модель потребитель/ производитель:

import logging
import threading
import time

LOG_FORMAT = '%(asctime)s %(threadName)-17s %(levelname)-8s %\
             (message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)

items = []
condition = threading.Condition()

class Consumer(threading.Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def consume(self):

        with condition:

            if len(items) == 0:
                logging.info('no items to consume')
                condition.wait()

            items.pop()
            logging.info('consumed 1 item')

            condition.notify()

    def run(self):
        for i in range(20):
            time.sleep(2)
            self.consume()

class Producer(threading.Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def produce(self):

        with condition:

            if len(items) == 10:
                logging.info('items produced {}.\
                    Stopped'.format(len(items)))
                condition.wait()

            items.append(1)
            logging.info('total items {}'.format(len(items)))

            condition.notify()

    def run(self):
        for i in range(20):
            time.sleep(0.5)
            self.produce()

Как это работает...

producer постоянно производит необходимый элемент и сохраняет его в имеющемся буфере. В то же самое время, consumer время от времени использует произведённые данные, удаляя их из общего буфера.

Как только некий consumer прихватывает какой- то объект из общего буфера, он пробуждает producer, который начнёт снова заполнять общий буфер.

Аналогично, consumer будет приостанавливаться если общий буфер пуст. Как только producer выгрузил необходимые данные в имеющийся буфер, consumer будет пробуждён.

Как вы можете видеть, и в этом случае также, надлежащее использование имеющейся директивы condition делает возможной должную синхронизацию существующих потоков.

Вот результат, который мы получили после отдельного исполнения:

2019-08-05 14:33:44,285 Producer INFO total items 1
2019-08-05 14:33:44,786 Producer INFO total items 2
2019-08-05 14:33:45,286 Producer INFO total items 3
2019-08-05 14:33:45,786 Consumer INFO consumed 1 item
2019-08-05 14:33:45,787 Producer INFO total items 3
2019-08-05 14:33:46,287 Producer INFO total items 4
2019-08-05 14:33:46,788 Producer INFO total items 5
2019-08-05 14:33:47,289 Producer INFO total items 6
2019-08-05 14:33:47,787 Consumer INFO consumed 1 item
2019-08-05 14:33:47,790 Producer INFO total items 6
2019-08-05 14:33:48,291 Producer INFO total items 7
2019-08-05 14:33:48,792 Producer INFO total items 8
2019-08-05 14:33:49,293 Producer INFO total items 9
2019-08-05 14:33:49,788 Consumer INFO consumed 1 item
2019-08-05 14:33:49,794 Producer INFO total items 9
2019-08-05 14:33:50,294 Producer INFO total items 10
2019-08-05 14:33:50,795 Producer INFO items produced 10. Stopped
2019-08-05 14:33:51,789 Consumer INFO consumed 1 item
2019-08-05 14:33:51,790 Producer INFO total items 10
2019-08-05 14:33:52,290 Producer INFO items produced 10. Stopped
2019-08-05 14:33:53,790 Consumer INFO consumed 1 item
2019-08-05 14:33:53,790 Producer INFO total items 10
2019-08-05 14:33:54,291 Producer INFO items produced 10. Stopped
2019-08-05 14:33:55,790 Consumer INFO consumed 1 item
2019-08-05 14:33:55,791 Producer INFO total items 10
2019-08-05 14:33:56,291 Producer INFO items produced 10. Stopped
2019-08-05 14:33:57,791 Consumer INFO consumed 1 item
2019-08-05 14:33:57,791 Producer INFO total items 10
2019-08-05 14:33:58,292 Producer INFO items produced 10. Stopped
2019-08-05 14:33:59,791 Consumer INFO consumed 1 item
2019-08-05 14:33:59,791 Producer INFO total items 10
2019-08-05 14:34:00,292 Producer INFO items produced 10. Stopped
2019-08-05 14:34:01,791 Consumer INFO consumed 1 item
2019-08-05 14:34:01,791 Producer INFO total items 10
2019-08-05 14:34:02,291 Producer INFO items produced 10. Stopped
2019-08-05 14:34:03,791 Consumer INFO consumed 1 item
2019-08-05 14:34:03,792 Producer INFO total items 10
2019-08-05 14:34:05,792 Consumer INFO consumed 1 item
2019-08-05 14:34:07,793 Consumer INFO consumed 1 item
2019-08-05 14:34:09,794 Consumer INFO consumed 1 item
2019-08-05 14:34:11,795 Consumer INFO consumed 1 item
2019-08-05 14:34:13,795 Consumer INFO consumed 1 item
2019-08-05 14:34:15,833 Consumer INFO consumed 1 item
2019-08-05 14:34:17,833 Consumer INFO consumed 1 item
2019-08-05 14:34:19,833 Consumer INFO consumed 1 item
2019-08-05 14:34:21,834 Consumer INFO consumed 1 item
2019-08-05 14:34:23,835 Consumer INFO consumed 1 item

Также ознакомьтесь...

Интересно рассмотреть внутренний механизм Python для данного механизма синхронизации условием. Имеющийся class _Condition создаёт некий объект RLock() если конструктору этого класса не передана никакая имеющаяся блокировка. Кроме того, эта управление блокировкой будет осуществляться при вызовах acquire() и released():

class _Condition(_Verbose):
    def __init__(self, lock=None, verbose=None):
        _Verbose.__init__(self, verbose)
        if lock is None:
            lock = RLock()
        self.__lock = lock

Синхронизация потоков событием

Некое событие является объектом, который применяется для взаимодействия между потоками. Поток может ожидать сигнал, в то время как другой поток выдаёт его. По существу, объект event управляет внутренним флагом, который может устанавливаться в false через clear() и устанавливаться в true при помощи set(), а также выполнять проверку посредством is_set().

Некий поток может фиксировать сигнал посредством метода wait(), который отправляет вызов через метод set().

Приготовление

Чтобы уяснить синхронизацию потоков через объект event, давайте снова рассмотрим задачу производитель/ потребитель.

Как это сделать...

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

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

Теперь давайте рассмотрим как реализовать эту задачу потребитель, производитель воспользовавшись синхронизацией при помощи оператора event:

Сначала мы импортируем относящиеся к делу библиотеки:

import logging
import threading
import time
import random

Далее мы определяем необходимый формат вывода регистрационных записей. Это полезно для ясной визуализации того что происходит:

LOG_FORMAT = '%(asctime)s %(threadName)-17s %(levelname)-8s %\
             (message)s'
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)

Устанавливаем необходимый список items. Этот параметр будет применяться в классах Consumer и Producer:

items = []

Ниже определяется параметр event. Этот параметр будет применяться для синхронизации необходимого взаимодействия между потоками:

event = threading.Event()

Наш класс Consumer инициализируется необходимым списком элементов и функциейEvent(). В методе run этот потребитель жидает потребления какого- то нового элемента. Когда такой элемент появляется, он выталкивается из общего списка item:

class Consumer(threading.Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def run(self):
        while True:
            time.sleep(2)
            event.wait()
            item = items.pop()
            logging.info('Consumer notify: {} popped by {}'\
                        .format(item, self.name))

Соответствующий класс Producer инициаизируется заданием списка элементов и функции Event(). В отличии от примера с объектами condition, данный список элементов не глобальный, а вместо этого он передаётся в качестве параметра:

class Producer(threading.Thread):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

В методе run для каждого созданного элемента наш класс Producer добавляет его в конец ощего списка элементов, а затем выставляет уведомление данного события:

def run(self):
        for i in range(5):
            time.sleep(2)
            item = random.randint(0, 100)
            items.append(item)
            logging.info('Producer notify: item {} appended by\
                         {}'\.format(item, self.name))

Имеются два шага, которые требуется предпринять для этого и наш первый шаг такой:

event.set()
event.clear()

Поток t1 добавляет в конец имеющегося списка некий элемент, а затем устанавливает событие, уведомляющее своего потребителя. Вызов потребителя wait() перестаёт блокироваться и соответствующее целое значение выбирается из общего списка:

if __name__ == "__main__":
    t1 = Producer()
    t2 = Consumer()

    t1.start()
    t2.start()

    t1.join()
    t2.join()

Как это работает...

Все операции между классами Producer и Consumer могут быть проще резюмированы при помощи следующей схемы:

Рисунок 2-2

Синхронизация потоков при помощи объектов event

В частности, классы Producer и Consumer обладают следующим поведением:

Producer овладевает некой блокировкой, добавляет элемент в имеющуюся очередь и уведомляет об этом Consumer сообщением (set event). Далее он засыпает до тех пор, пока не получит новый элемент на добалвение.

Consumer овладевает блокировкой, а затем начинает выполнять ожидание входящих элементов в непрерывном цикле. В тот момент, когда появляется соответствующее событие, этот потребитель обрывает блокировку с тем, чтобы другой производитель/ потребитель вошёл и владел этой блокировкой. Когда Consumer повторно активируется, он тогда повторно овладевает блокировкой, безопасно обрабатывая новый элементы из общей очереди:

2019-02-02 18:23:35,125 Thread-1 INFO Producer notify: item 68 appended by Thread-1
2019-02-02 18:23:35,133 Thread-2 INFO Consumer notify: 68 popped by Thread-2
2019-02-02 18:23:37,138 Thread-1 INFO Producer notify: item 45 appended by Thread-1
2019-02-02 18:23:37,143 Thread-2 INFO Consumer notify: 45 popped by Thread-2
2019-02-02 18:23:39,148 Thread-1 INFO Producer notify: item 78 appended by Thread-1
2019-02-02 18:23:39,153 Thread-2 INFO Consumer notify: 78 popped by Thread-2
2019-02-02 18:23:41,158 Thread-1 INFO Producer notify: item 22 appended by Thread-1
2019-02-02 18:23:43,173 Thread-1 INFO Producer notify: item 48 appended by Thread-1
2019-02-02 18:23:43,178 Thread-2 INFO Consumer notify: 48 popped by Thread-2

Синхронизация потоков барьером

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

Приготовление

Модуль потоков Python реализует барьеры посредством класса Barrier. В своём следующем разделе давайте изучим как применять этот механизм синхронизации в очень простом примере.

Как это сделать...

В этом примере мы имитируем забег трёх участников, Huey, Dewey и Louie, при котором некий барьер уподобляется финишной линии.

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

Наш барьер реализуется классом Barrier, в котором общее число подлежащих выполнению потоков для перехода к следующему этапу должно быть указано в качестве некого аргумента:

from random import randrange
from threading import Barrier, Thread
from time import ctime, sleep

num_runners = 3
finish_line = Barrier(num_runners)
runners = ['Huey', 'Dewey', 'Louie']

def runner():
    name = runners.pop()
    sleep(randrange(2, 5))
    print('%s reached the barrier at: %s \n' % (name, ctime()))
    finish_line.wait()

def main():
    threads = []
    print('START RACE!!!!')
    for i in range(num_runners):
        threads.append(Thread(target=runner))
        threads[-1].start()
    for thread in threads:
        thread.join()
    print('Race over!')

if __name__ == "__main__":
    main()

Как это работает...

Прежде всего мы устанавливаем значение числа бегунов в num_runners = 3 чтобы задать конечную цель на нашей первой линии через соответствующую директиву Barrier. Наши бегуны установлены в соответствующем списке забега; каждый из них будет иметь время появления, которое определяется значением функции runner, использующей директиву randrange.

Когда некий бегун достигает финиша, он вызывает метод wait, который заблокирует всех тех бегунов (имеющиеся потоки) которые сделали этот вызов. Вывод для данного случая таков:

START RACE!!!!
Dewey reached the barrier at: Sat Feb 2 21:44:48 2019

Huey reached the barrier at: Sat Feb 2 21:44:49 2019

Louie reached the barrier at: Sat Feb 2 21:44:50 2019

Race over!

В нашем варианте забег выиграл Dewey.

Синхронизация потоков очередью

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

Тем не менее, рекомендуется применять модуль queue. На самом деле, некая очередь намного проще для работы и делает потоковое программирование относительно безопаснее, поскольку она увлекает в одну воронку весь доступ к некому ресурсу отдельного потока и обеспечивает чёткий и более читаемый шаблон разработки.

Приготовление

Мы просто рассмотрим эти методы очереди:

put(): помещает элемент в очередь

get(): удаляет элемент из очереди и возвращает его

task_done(): требуется вызывать всякий раз при воспроизведении некого элемента

join(): выполняет блокировку пока не обработаны все элементы

Как это сделать...

В данном примере мы рассмотрим как применять модуль threading с модулем queue. Кроме того, здесь у нас имеются два логических элемента, которые пытаются разделять некий общий ресурс, очередь. Код таков:

from threading import Thread
from queue import Queue
import time
import random

class Producer(Thread):
    def __init__(self, queue):
        Thread.__init__(self)
        self.queue = queue
    def run(self):
        for i in range(5):
            item = random.randint(0, 256)
            self.queue.put(item)
            print('Producer notify : item N°%d appended to queue by\
                  %s\n'\
                  % (item, self.name))
            time.sleep(1)

class Consumer(Thread):
    def __init__(self, queue):
        Thread.__init__(self)
        self.queue = queue

    def run(self):
        while True:
            item = self.queue.get()
            print('Consumer notify : %d popped from queue by %s'\
                  % (item, self.name))
            self.queue.task_done()

if __name__ == '__main__':
    queue = Queue()
    t1 = Producer(queue)
    t2 = Consumer(queue)
    t3 = Consumer(queue)
    t4 = Consumer(queue)

    t1.start()
    t2.start()
    t3.start()
    t4.start()

    t1.join()
    t2.join()
    t3.join()
    t4.join()

Как это работает...

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

Наш поток класса producer вырабатывает целые и помещает их в установленную очередь в цикле for. Класс producer применяет Queue.put(item[, block[, timeout]]) для вставки данных в общую очередь. Этот метод обладает необходимой логикой овладения необходимой блокировкой перед вставкой данных в некую очередь.

Имеются две возможности:

Если значениями необязательных параметров block выступает true, а timeout равен None (это устанавливаемый по умолчанию вариант и мы именно его применяем в своём примере), тогда нам требуется выполнять блокирование пока не станет доступным некий свободный слот. Когда значением таймаута является некое положительное число, тогда блокирование осуществляется по крайней мере timeout секунд и выставляется полная исключительная ситуация если за это время не стал доступным никакой свободный слот.

Когда значением block является false, тогда если немедленно доступен какой- то свободный слот, в него помещается некий элемент, в противном случае вызывается полная исключительная ситуация (значение timeout в этом случае игнорируется). Здесь put проверяет будет ли наша очередь заполнена и затем вызывает wait внутренним образом, после чего соответствующий производитель начинает ожидание.

Следующим идёт класс consumer. Его поток получает значение целого из установленной очереди и указывает что это выполнено при помощи task_done. Наш класс consumer применяет Queue.get([block[, timeout]]) и овладевает необходимой блокировкой перед тем как удалять данные из этой очереди. Этот потребитель помещается в состояние ожидания в том случае, когда данная очередь пуста. Наконец, в своей функции main мы создаём четыре потока, причём один для класса producer и три для класса consumer соответственно.

Получаемый вывод должен выглядеть как- то так:

Producer notify : item N°186 appended to queue by Thread-1
Consumer notify : 186 popped from queue by Thread-2

Producer notify : item N°16 appended to queue by Thread-1
Consumer notify : 16 popped from queue by Thread-3

Producer notify : item N°72 appended to queue by Thread-1
Consumer notify : 72 popped from queue by Thread-4

Producer notify : item N°178 appended to queue by Thread-1
Consumer notify : 178 popped from queue by Thread-2

Producer notify : item N°214 appended to queue by Thread-1
Consumer notify : 214 popped from queue by Thread-3

Также ознакомьтесь...

Все операции между нашими классами Producer и Consumer запросто можно свести воедино на следующей схеме:

Рисунок 2-3

Синхронизация потоков при помощи модуля queue

Наш поток Producer овладевает необходимой блокировкой и затем вставляет данные в структуру данных QUEUE.

Созданные потоки Consumer: получают целые значения из общей QUEUE. Эти потоки овладевают необходимой блокировкой прежде чем удалять данные из общей QUEUE.

Когда QUEUE пуста, потоки consumer получают состояние ожидающих.

На этом рецепте данная глава, посвящённая праллелизму на основе потоков подошла к концу.

Глава 3. Параллельность на основе процессов

Содержание

Глава 3. Параллельность на основе процессов

Основы модуля множественности процессов Python

Порождение процесса

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Именование процесса

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Выполнение процессов в фоновом режиме

Приготовление

Как это сделать...

Как это работает...

Дополнительно

Уничтожение процессов

Приготовление

Как это сделать...

Как это работает...

Дополнительно

Определение процессов в неком подклассе

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Применение очереди для обмена данными

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Применение конвейеров для обмена объектами

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Синхронизация процессов

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Применение пула процессов

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

В своей предыдущей главе мы изучили как применять потоки для реализации одновременных приложений. Данная глава изучит имеющийся подход на основе процессов, который был введён в Главе 1, Приступая к параллельному программированию и Python. В особенности, в этой главе мы сосредоточимся на модуле Python multiprocessing.

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

В этой главе мы рассмотрим следующие рецепты:

Основы модуля Python multiprocessing

Порождение некого процесса

Именование процесса

Запуск процесса в фоновом режиме

Уничтожение процесса

Определение процесса в каком- то подчинённом классе

Применение очереди для обмены объектами

Использование конвейеров для обмена объектами

Синхронизация процессов

Управление состояниыми процессов

Использование пула процессов

Основы модуля множественности процессов Python

Во введении в документацию Python multiprocessing чётко указывается тот факт, что вся имеющаяся внутри этого пакета функциональность требует того, чтобы модуль main был доступен для импорта в его потомков (https://docs.python.org/3.3/library/multiprocessing.html).

Имеющийся в IDLE модуль __main__ не доступен для импорта в потомков, даже когда он запускается как определённый сценарий в качестве файла в IDLE. Для получения необходимого правильного результата мы будем запускать все свои примеры из приглашения Командной строки:

> python multiprocessing_example.py

Здесь multiprocessing_example.py является названием соотвествующего сценария.

Порождение процесса

Порождение некого процесса это собственно создание дочернего процесса из родительского процесса. Последний затем продолжает своё выполнение асинхронно или ожидает пока этот дочерний процесс не завершится.

Приготовление

Библиотека multiprocessing позволяет порождать процессы такой последовательностью:

Определяем объект process.

Вызываем метод start() того процесса, который его запускает.

Вызываем метод join() этого процесса. Он ожидает завершение соответствующего задания этого процесса и затем выполняем выход.

Как это сделать...

Давайте рассмотрим следующие шаги:

Для создания некого процесса нам требуется импортировать сам модуль multiprocessing при помощи такой команды:

import multiprocessing

Все процессы ассоциированы с определённой функцией myFunc(i). Данная функция выводи числа от 0 до i, где i является тем идентификаторов, который ассоциирован самому номеру процесса:

def myFunc(i):
    print ('calling myFunc from process n°: %s' %i)
    for j in range (0,i):
        print('output from myFunc is :%s' %j)

Затем мы определяем необходимый объект process с myFunc в качестве функции target:

if __name__ == '__main__':
    for i in range(6):
        process = multiprocessing.Process(target=myFunc, args=(i,))

Наконец, мы вызываем необходимые методы start и join в этом созданном процессе:

process.start()
process.join()

Без наличия такого метода join дочерний процесс не завершится и его потребуется уничтожать вручную.

Как это работает...

В этом разделе мы таким образом увидели как можно создавать процесс запуская некий дочерний процесс. Такая функциональность именуется порождением процесса (spawning a process).

Библиотека Python multiprocessing делает возможным простое управление следующими тремя простыми шагами. Самый первый шаг состоит в определении самого процесса через метод класса multiprocessing, Process:

process = multiprocessing.Process(target=myFunc, args=(i,))

Этот метод Process имеет некий параметр подлежащей порождению функции, myFunc, а также все параметры внутри самой этой функции.

Следующие два шага необходимы для выполнения данного процесса и выхода из него:

process.start()
process.join()

Чтобы запустить необходимый процесс и отобразить его результаты, давайте откроем приглашение Командной строки, причём предпочтительно в том же самом каталоге, который содержит наш фал примера (spawning_processes.py) и затем наберём следующую команду:

> python spawning_processes.py

Для всех созданных процессов (их всего шесть) показывается получаемый вывод целевой функции. Помните, что это некий простейший счётчик от нуля до значения индекса установленного идентификатора процесса:

calling myFunc from process n°: 0
calling myFunc from process n°: 1
output from myFunc is :0
calling myFunc from process n°: 2
output from myFunc is :0
output from myFunc is :1
calling myFunc from process n°: 3
output from myFunc is :0
output from myFunc is :1
output from myFunc is :2
calling myFunc from process n°: 4
output from myFunc is :0
output from myFunc is :1
output from myFunc is :2
output from myFunc is :3
calling myFunc from process n°: 5
output from myFunc is :0
output from myFunc is :1
output from myFunc is :2
output from myFunc is :3
output from myFunc is :4

Также ознакомьтесь...

Это вновь напоминает нам о важности создания экземпляра необходимого объекта Process внутри самого главного раздела: это происходит по той причине, что создаваемый дочерний процесс импортирует сам файл сценария, который содержит необходимую функцию target. Затем, порождая соответствующий объект process внутри этого блока, мы предотвращаем некий бесконечный рекурсивный вызов такого порождения экземпляров.

Для задания самой функции target в каком- то ином сценарии применяется некий допустимый обходной путь, а именно, myFunc.py:

def myFunc(i):
    print ('calling myFunc from process n°: %s' %i)
    for j in range (0,i):
        print('output from myFunc is :%s' %j)
    return

Сама программа main, содержащая экземпляр головного процесса определяется в неком втором файле (target):

import multiprocessing
from myFunc import myFunc

if __name__ == '__main__':
    for i in range(6):
        process = multiprocessing.Process(target=myFunc, args=(i,))
        process.start()
        process.join()

Для запуска этого примера наберите следующую команду:

> python spawning_processes_names.py

Получаемый вывод будет тем же самым что и у нашего предыдущего примера.

Дополнительно

Официальное руководство по этой библиотеке multiprocessing можно найти по ссылке.

Именование процесса

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

Приготовление

В некотором месте вашего кода может быть критически важным знать какой именно процесс исполняется в настоящий момент. Для этой цели обсуждаемая нами библиотека multiprocessing предоставляет свой метод current_process(), который применяет значение атрибута name для определения того какой процесс исполняется в данный момент. В своём следующем разделе мы рассмотрим эту тему.

Как это сделать...

Давайте выполним такие шаги:

Значением функции target для обоих процессов выступает функция myFunc. Она выводит соответствующее название процесса вычисляя метод multiprocessing.current_process().name:

import multiprocessing
import time

def myFunc():
    name = multiprocessing.current_process().name
    print ("Starting process name = %s \n" %name)
    time.sleep(3)
    print ("Exiting process name = %s \n" %name)

Затем мы создаём process_with_name просто создавая экземпляры со значением параметра name и process_with_default_name:

if __name__ == '__main__':
    process_with_name = multiprocessing.Process\
                        (name='myFunc process',\
                          target=myFunc)

    process_with_default_name = multiprocessing.Process\
                                (target=myFunc)

Наконец, эти процессы запускаются и затем присоединяются:

process_with_name.start()
    process_with_default_name.start()
    process_with_name.join()
    process_with_default_name.join()

Как это работает...

В нашей программе main все процессы создаются с применением одной и той же целевой функции, myFunc. Эта функция просто выводит на печать название своего процесса.

Для запуска нашего примера откройте приглашение Командной строки и наберите такую команду:

> python naming_processes.py

Получаемый вывод будет примерно таким:

Starting process name = myFunc process
Starting process name = Process-2

Exiting process name = Process-2
Exiting process name = myFunc process

Также ознакомьтесь...

Основным процессом Python выступает multiprocessing.process._MainProcess, в то время как дочерним процессом является multiprocessing.process.Process. Это можно запросто проверить набрав следующее:

>>> import multiprocessing
>>> multiprocessing.current_process().name
'MainProcess'

Дополнительно

Дополнительные сведения по этой теме можно найти по ссылке.

Выполнение процессов в фоновом режиме

Запуск в фоновом режиме является состоянием исполнения, которое типично для некоторых программ, которым не требуется присутствие определённого пользователя или его вмешательство и которое может быть одновременным с выполнением прочих программ (и, тем самым, он возможен лишь в многозадачных системах), что имеет результатом тот факт, что пользователь не осведомлён о ней. Программы фонового режима обычно выполняют длинные или потребляющие много времени задачи, такие как программы однорангового совместного использования файлов или дефрагментация файловых систем. Многие процессы ОС также выполняются в фоновом режиме.

В Windows программы в таком режиме (сканирующие антивирусы или обновляющие ОС) зачастую помещают некую иконку в системном лотке (system tray, та область на рабочем столе, которая следует за системными часами) чтобы сигнализировать о своей активности и приспосабливать поведение, которое снижает потребление ресурсов с тем, чтобы не оказывать влияния ни интерактивную деятельность самого пользователя, например, замедление или вызывание прерываний. В системах Unix или Unix- подобных, работающие в фоновом режиме процессы называются демонами. Воспользовавшись диспетчера задач можно выделить все исполняющиеся программы, включая те, которые работают в фоновом режиме.

Приготовление

Наш модуль multiprocessing позволяет - через свой демонический вариант - запускать процессы в фоновом режиме. В нашем следующем примере определяются два процесса:

background_process с его параметром daemon, настроенным в значение False.

NO_background_process с его параметром daemon, настроенным в значение True.

Как это сделать...

В своём следующем примере мы реализуем некую целевую функцию, с названием foo, которая отображает цифры от 0 до 4, когда её дочерний процесс пребывает в фоновом режиме; в противном случае, она выводит на печать цифры от 5 до 9:

Давайте импортируем относящиеся к делу библиотеки:

import multiprocessing
import time

Затем мы определяем свою функцию foo(). Как это описано ранее, выводимые на печать цифры зависят от значения параметра name:

def foo():
    name = multiprocessing.current_process().name
    print ("Starting %s \n" %name)
    if name == 'background_process':
        for i in range(0,5):
            print('---> %d \n' %i)
        time.sleep(1)
    else:
        for i in range(5,10):
            print('---> %d \n' %i)
        time.sleep(1)
    print ("Exiting %s \n" %name)

Наконец, мы определяем такие процессы: background_process и NO_background_process. Обратите внимание, что значение параметра daemon устанавливается для обоих этих процессов:

if __name__ == '__main__':
    background_process = multiprocessing.Process\
                         (name='background_process',\
                          target=foo)
    background_process.daemon = True

    NO_background_process = multiprocessing.Process\
                            (name='NO_background_process',\
                             target=foo)

    NO_background_process.daemon = False

    background_process.start()
    NO_background_process.start()

Как это работает...

Заметим, что значение параметра daemon самого процесса определяет должен ли этот процесс запускаться в фоновом режиме или нет. Для исполнения данного примера наберите такую команду:

> python run_background_processes.py

Наш вывод очевидно выдаёт отчёт только о выводе NO_background_process:

Starting NO_background_process
---> 5

---> 6

---> 7

---> 8

---> 9
Exiting NO_background_process

Сам вывод изменяет соответствующую настройку значения параметра daemon для background_process на False:

background_process.daemon = False

Для исполнения этого примера наберите следующее:

C:\> python run_background_processes_no_daemons.py

Получаемый вывод выдаёт отчёт о выполнении обоих процессов, и background_process, и NO_background_process:

Starting NO_background_process
Starting background_process
---> 5

---> 0
---> 6

---> 1
---> 7

---> 2
---> 8

---> 3
---> 9

---> 4

Exiting NO_background_process
Exiting background_process

Дополнительно

Некий фрагмент кода для того как запускать некий сценарий Python в фоновом режиме в Linux можно найти по ссылке.

Уничтожение процессов

Не существует безупречного программного обеспечения и даже в самое наилучшее приложение вы можете вложить некую ошибку, которая приводит к блокировке данного приложения и именно по этой причине современные ОС имеют разработанными различные методы прекращения таких процессов приложений чтобы высвобождать системные ресурсы и позволять своему пользователю применять их для других операций настолько быстро, как только это возможно. Данный раздел покажет вам как уничтожать (kill) некий процесс в вашем приложении со множеством процессов.

Приготовление

Имеется возможность немедленного уничтожения процесса при помощи метода terminate. Кроме того мы воспользуемся методом is_alive для отслеживания того жив ли конкретный процесс или нет.

Как это сделать...

Для осуществления данного рецепта следуйте приводимым ниже шагам:

Давайте импортируем относящиеся к делу библиотеки:

import multiprocessing
import time

Затем реализуется некая простейшая функция target. В этом примере наша функция target, foo() выводит на печать первые 10 цифр:

def foo():
    print ('Starting function')
    for i in range(0,10):
        print('-->%d\n' %i)
        time.sleep(1)
    print ('Finished function')

В своей программе main мы создаём некий процесс отслеживания её времени жизни посредством метода is_alive; далее мы завершаем её при помощи вызова terminate;

if __name__ == '__main__':
    p = multiprocessing.Process(target=foo)
    print ('Process before execution:', p, p.is_alive())
    p.start()
    print ('Process running:', p, p.is_alive())
    p.terminate()
    print ('Process terminated:', p, p.is_alive())
    p.join()
    print ('Process joined:', p, p.is_alive())

После этого мы проверяем значение кода состояния когда наш процесс завершён и считываем значение атрибута значения ExitCode своего процесса:

print ('Process exit code:', p.exitcode)

Возможными значениями ExitCode являются такие:

== 0: Не воспроизведена никакая ошибка

> 0: Наш процесс имел некую ошибку и вышел с данным кодом

< 0: Наш процесс был уничтожен сигналом -1 * ExitCode.

New-VM -Name VM01 -Generation 2

Как это работает...

Наш образец кода состоит из некой целевой функции, foo(), задача которой состоит в выводе на экран печати самых первых 10 целых чисел. В нашей программе main этот процесс исполняется, а затем уничтожается инструкцией terminate. Этот процесс затем присоединяется и определяется ExitCode.

Для выполнения данного кода наберите такую команду:

> python killing_processes.py

После этого мы получаем следующий вывод:

Process before execution: <Process(Process-1, initial)> False
Process running: <Process(Process-1, started)> True
Process terminated: <Process(Process-1, started)> True
Process joined: <Process(Process-1, stopped[SIGTERM])> False
Process exit code: -15

Обратите внимание, что величина выводимого значения ExitCode равна -15. Отрицательность значения -15 указывает на то, что наш потомок был уничтожен сигналом прерывания, который определяется его числовым значением 15.

Дополнительно

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

Определение процессов в неком подклассе

Модуль multiprocessing предоставляет доступ к функциям управления процессом. В данном разделе мы изучим как определять некий процесс в каком- то подклассе имеющегося класса multiprocessing.Process.

Приготовление

Для реализации некого индивидуального подкласса многопроцессности нам требуется выполнить следующие моменты:

Задать некий подкласс класса multiprocessing.Process, переопределив его метод run().

Перекрыть метод _init__(self [,args]) для добавления дополнительных параметров, если это требуется.

Перекрыть метод run(self [,args]) для реализации того что Process должен выполнять при своём запуске.

После того как мы создали нужный нам новый подкласс Process, мы можем создать некий его экземпляр и затем запустить, вызвав соответствующий метод start, который, в свою очередь, вызывает установленный метод run.

Как это сделать...

Просто рассмотрим очень простой пример:

Импортируем относящуюся к делу библиотеку:

import multiprocessing
 	   >

Далее определяем некий подкласс, MyProcess, перекрывая лишь его метод run, который возвращает название своего процесса:

class MyProcess(multiprocessing.Process):

    def run(self):
        print ('called run method by %s' %self.name)
        return

В своей программе main мы определим некий подкласс из 10 процессов:

if __name__ == '__main__':
    for i in range(10):
        process = MyProcess()
        process.start()
        process.join()

Как это работает...

Каждый процесс подкласса представлен неким классом, который расширяет установленный класс Process и перекрывает его метод run(). Данный метод является отправной точкой Process:

class MyProcess (multiprocessing.Process):
    def run(self):
        print ('called run method in process: %s' %self.name)
        return

В своей программе main мы создаём некоторые объекты с типом Process(). Исполнение данного потока начинается после вызова метода start():

p = MyProcess()
p.start()

Наша команда join() просто обрабатывает сам процесс останова. Для выполнения этого сценария из приглашения Командной строки наберите такую команду:

> python process_in_subclass.py

Её вывод будет следующим:

called run method by MyProcess-1
called run method by MyProcess-2
called run method by MyProcess-3
called run method by MyProcess-4
called run method by MyProcess-5
called run method by MyProcess-6
called run method by MyProcess-7
called run method by MyProcess-8
called run method by MyProcess-9
called run method by MyProcess-10

Также ознакомьтесь...

В объектно ориентированном программировании некий подкласс является классом, который наследует все свойства из некого суперкласса, которыми выступают объекты и методы. Неким альтернативным названием подкласса является порождаемый класс (derived class). Наследование (inheritance) является специфическим термином, который указывает на тот процесс, коим его потомок или порождаемые классы наследуют свои свойства из родительского класса или суперклассов.

Вы можете представлять себе подкласс как определённую разновидность его суперкласса; на самом деле он может применять методы и/ или атрибуты, а также переопределять их путём перекрытия (overriding).

Дополнительно

Дополнительные сведения по технологиям определения классов можно найти по ссылке.

Применение очереди для обмена данными

Некая очередь является структурой данных с типом FIFO (First-In, First-Out, первый пришедший обслуживается первым). Неким примером из практики являются очереди на получение обслуживание, как производится оплата в супермаркете или ваша стрижка в парикмахерской. В идеале вы обслуживаетесь точно в том порядке, в котором вы были представлены. Именно так и работает очередь FIFO.

Приготовление

В данном разделе мы покажем вам как применять очередь для задачи производитель- потребитель, которая является классическим примером синхронизации процессов.

Задача производитель- потребитель описывает два процесса: один выступаетпроизводителем, а другой является потребителем, и они совместно используют общий буфер с неким фиксированным размером.

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

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

Как это сделать...

Это решение может быть реализовано посредством стратегий взаимодействия между процессами, совместной памятью или обменом сообщениями. Неверное решение может приводить к взаимной блокировке (deadlock), при которой оба процесса ожидают пробуждения.

import multiprocessing
import random
import time

Давайте выполним следующее:

Наш класс producer отвечает за ввод 10 элементов в имеющуюся очередь при помощи своего метода put:

class producer(multiprocessing.Process):
    def __init__(self, queue):
        multiprocessing.Process.__init__(self)
        self.queue = queue

    def run(self) :
        for i in range(10):
            item = random.randint(0, 256)
            self.queue.put(item)
            print ("Process Producer : item %d appended \
                   to queue %s"\
                   % (item,self.name))
            time.sleep(1)
            print ("The size of queue is %s"\
                   % self.queue.qsize())

Класс consumer имеет задачу удаления элементов из общей очереди (с помощью метода get) и проверки того что эта очередь не пуста. Когда это происходит, тогда установленный внутри его цикла while поток останавливается оператором break:

class consumer(multiprocessing.Process):
    def __init__(self, queue):
        multiprocessing.Process.__init__(self)
        self.queue = queue

    def run(self):
        while True:
            if (self.queue.empty()):
                print("the queue is empty")
                break
            else :
                time.sleep(2)
                item = self.queue.get()
                print ('Process Consumer : item %d popped \
                        from by %s \n'\
                       % (item, self.name))
                time.sleep(1)

Наш класс multiprocessing имеет свой объект queue, устанавливаемый в основной программе main:

if __name__ == '__main__':
        queue = multiprocessing.Queue()
        process_producer = producer(queue)
        process_consumer = consumer(queue)
        process_producer.start()
        process_consumer.start()
        process_producer.join()
        process_consumer.join()

Как это работает...

Внутри своей программы main мы определяем необходимую очередь с помощью объекта multiprocessing.Queue. Затем он передаётся в качестве некого аргумента в процессы producer и consumer:

queue = multiprocessing.Queue()
process_producer = producer(queue)
process_consumer = consumer(queue)

В нашем классе producer метод queue.put применяется для добавления в конец общей очереди новых элементов:

self.queue.put(item)

В то же время в классе consumer его метод queue.get применяется для вытаскивания имеющихся элементов:

self.queue.get()

Выполните полученный код набрав следующую команду:

> python communicating_with_queue.py

Приводимый ниже вывод является отчётом о взаимодействии между нашими производителем и потребителем:

Process Producer : item 79 appended to queue producer-1
The size of queue is 1
Process Producer : item 50 appended to queue producer-1
The size of queue is 2
Process Consumer : item 79 popped from by consumer-2
Process Producer : item 33 appended to queue producer-1
The size of queue is 2
Process Producer : item 57 appended to queue producer-1
The size of queue is 3
Process Producer : item 227 appended to queue producer-1
Process Consumer : item 50 popped from by consumer-2
The size of queue is 3
Process Producer : item 98 appended to queue producer-1
The size of queue is 4
Process Producer : item 64 appended to queue producer-1
The size of queue is 5
Process Producer : item 182 appended to queue producer-1
Process Consumer : item 33 popped from by consumer-2
The size of queue is 5
Process Producer : item 206 appended to queue producer-1
The size of queue is 6
Process Producer : item 214 appended to queue producer-1
The size of queue is 7
Process Consumer : item 57 popped from by consumer-2
Process Consumer : item 227 popped from by consumer-2
Process Consumer : item 98 popped from by consumer-2
Process Consumer : item 64 popped from by consumer-2
Process Consumer : item 182 popped from by consumer-2
Process Consumer : item 206 popped from by consumer-2
Process Consumer : item 214 popped from by consumer-2
the queue is empty

Также ознакомьтесь...

Очередь обладает неким подклассом JoinableQueue. Он предоставляет такие методы:

task_done(): Данный метод указывает что некая задача завершена, например, после применения метода get() для выборки элементов из этой очереди. По этой причине task_done() можно применять потребителям очереди.

join(): Данный метод блокирует все процессы до тех пор, пока все имеющиеся в этой очереди элементы не будут завершены и обработаны.

Дополнительно

Хорошее руководство относительно того как применять очереди доступно по ссылке.

Применение конвейеров для обмена объектами

Некий конвейер (pipe) выполняет следующее:

Возвращает пару соединённых объектов, объединяемых неким конвейером.

Каждый из присоединённых объектов обладает методами отправки/ получения для взаимодействия между процессами.

Приготовление

Рассматриваемая нами библиотека multiprocessing позволяет вам реализацию некой структуры данных конвейера при помощи своей функции multiprocessing.Pipe(duplex). Она возвращает некую пару объектов (conn1, conn2), которые представляют концы этого конвейера.

Значение параметра duplex будет ли данный конвейер для нашего последнего случая двунаправленным (то есть, duplex = True), или однонаправленным (то есть, duplex = False), когда conn1 может применяться лишь для получения сообщений, а conn1 только для отправки сообщений.

Теперь давайте рассмотрим как выполнять обмен сообшениями при помощи конвейеров.

Как это сделать...

Здесь приводится некий образец конвейеров. У нас имеется один процесс конвейера, который выводит числа от 0 до 9, а второй процесс конвейера получает числа и возводит их в квадрат:

Давайте импортируем библиотеку multiprocessing:

import multiprocessing

Наша функция pipe возвращает пару соединённых объектов, объединяемых двусторонним конвейером. В этом примере out_pipe содержит числа от 0 до 9, которые были выработаны функцией target из create_items:

def create_items(pipe):
    output_pipe, _ = pipe
    for item in range(10):
        output_pipe.send(item)
    output_pipe.close()

Следующая функция multiply_items основана на двух конвейерах, pipe_1 и pipe_2:

def multiply_items(pipe_1, pipe_2):
    close, input_pipe = pipe_1
    close.close()
    output_pipe, _ = pipe_2
    try:
        while True:
            item = input_pipe.recv()

Эта функция возвращает произведение всех поступающих из конвейера элементов:

           output_pipe.send(item * item)
 except EOFError:
        output_pipe.close()

В нашей программе main определяются pipe_1 и pipe_2:

if __name__== '__main__':

Первым процесс pipe_1 с числами от 0 до 9:

pipe_1 = multiprocessing.Pipe(True)
    process_pipe_1 = \
                   multiprocessing.Process\
                   (target=create_items, args=(pipe_1,))
    process_pipe_1.start()

Затем процесс pipe_2, который выхватывает поступающие из pipe_1 числа и возводит их в квадрат:

pipe_2 = multiprocessing.Pipe(True)
    process_pipe_2 = \
                   multiprocessing.Process\
                   (target=multiply_items, args=(pipe_1, pipe_2,))
    process_pipe_2.start()

Закрываем свои процессы:

pipe_1[0].close()
pipe_2[0].close()

Выводим на печать полученные результаты:

try:
        while True:
            print (pipe_2[1].recv())
    except EOFError:
        print("End")

Как это работает...

По существу, наши два конвейера, pipe_1 и pipe_2, создаются операторами multiprocessing.Pipe(True):

pipe_1 = multiprocessing.Pipe(True)
pipe_2 = multiprocessing.Pipe(True)

Самый первый конвейер, pipe_1 просто создаёт некий список целых с 0 до 9, в то время как наш второй конвейер, pipe_2, обрабатывает каждый элемент из этого списка, создаваемого pipe_1, вычисляя значение квадрата для каждого из элементов:

pipe_1 = multiprocessing.Pipe(True)
pipe_2 = multiprocessing.Pipe(True)

Следовательно, оба процесса закрываются:

pipe_1[0].close()
pipe_2[0].close()

И на печать выводится окончательный результат:

print (pipe_2[1].recv())

Выполним созданный код набрав такую команду:

> python communicating_with_pipe.py

Ниже приводится результат, отображающий квадраты первых 9 цифр:

0
1
4
9
16
25
36
49
64
81

Также ознакомьтесь...

Когда вам требуется для взаимодействия иметь более двух точек, пользуйтесь методом Queue(). Однако если вам необходима максимальная производительность, тогда имейте в виду, что метод Pipe() намного быстрее, потому что Queue() строится поверх Pipe().

Дополнительно

Дополнительные сведения относительно Python и конвейеров можно найти по ссылке.

Синхронизация процессов

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

Примитивы синхронизации таковы:

Lock: Этот объект может быть либо в заблокированном, либо в разблокированном состоянии. Некий заблокированный объект обладает двумя методами, acquire() и release() для управления доступом к некому совместному ресурсу.

Event: Данный объект реализует простое взаимодействие между процессами; один процесс выставляет сигналом некое событие, а другой ожидает его. Объект события имеет два метода, set() и clear() для управления своим собственным внутренним флагом.

Condition: Такой объект используется для синхронизации частей некого рабочего потока, причём как в последовательных, так и в параллельных процессах. Он имеет два основных метода: wait() применяется для ожидания некого условия, а notify_all() применяется для взаимодействия с тем условием, которое было применено.

Semaphore: Используется для разделения некого ресурса, например, для поддержки фиксированного числа одновременных подключений.Code

RLock: Задаёт объект рекурсивной блокировки. Все методы и функциональность RLock те же самые что и в обсуждавшемся уже модуле threading.

Barrier: Делит программу на фазы, которые требуют достижения всеми процессами установленного барьера прежде чем они могут быть продолжены. Исполняемый после барьера код не может быть одновременным с тем кодом, который исполнялся перед этим барьером.

Приготовление

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

Наш следующий пример показывает как выполнять синхронизацию одновременных задач при помощи объекта barrier().

Как это сделать...

Давайте рассмотрим четыре процесса, причём процессы p1 и p2 управляются неким оператором барьера, в то время как процессы p3 и p4 не обладают директивами синхронизации.

Для выполнения этого осуществите такие шаги:

Импортируем нужные нам библиотеки:

import multiprocessing
from multiprocessing import Barrier, Lock, Process
from time import time
from datetime import datetime

Наша функция test_with_barrier выполняет метод барьера wait():

def test_with_barrier(synchronizer, serializer):
    name = multiprocessing.current_process().name
    synchronizer.wait()
    now = time()

Когда наши два процесса вызвали метод wait(), они высвобождаются одновременно:

with serializer:
    print("process %s ----> %s" \
        %(name,datetime.fromtimestamp(now)))

def test_without_barrier():
    name = multiprocessing.current_process().name
    now = time()
    print("process %s ----> %s" \
        %(name ,datetime.fromtimestamp(now)))

В своей программе main мы создаём четыре процесса. Однако нам также требуются некие примитивы барьера и блокировки. Значение параметра 2 в операторе Barrier устанавливается для общего числа управляемых процессов:

if __name__ == '__main__':
    synchronizer = Barrier(2)
    serializer = Lock()
    Process(name='p1 - test_with_barrier'\
            ,target=test_with_barrier,\
            args=(synchronizer,serializer)).start()
    Process(name='p2 - test_with_barrier'\
            ,target=test_with_barrier,\
            args=(synchronizer,serializer)).start()
    Process(name='p3 - test_without_barrier'\
            ,target=test_without_barrier).start()
    Process(name='p4 - test_without_barrier'\
            ,target=test_without_barrier).start()

Как это работает...

Обсуждаемый объект Barrier предоставляет одну из технологий Python при которой один или множество потоков будут дожидаться в определённом месте в множестве действий и осуществлять развитие совместно.

В нашей программе main определяется необходимый объект Barrier (то есть synchronizer) следующим оператором:

synchronizer = Barrier(2)

Обратите вниме=ание, что значение числа 2 внутри скобок представляет общее число процессов, которого должен дожидаться этот барьер.

Затем мы реализуем некое множество из четырёх процессов, однако лишь p1 и p2 имеют барьер. Обратите внимание, что synchronizer передаётся в виде некого параметра:

Process(name='p1 - test_with_barrier'\
            ,target=test_with_barrier,\
            args=(synchronizer,serializer)).start()
Process(name='p2 - test_with_barrier'\
            ,target=test_with_barrier,\
            args=(synchronizer,serializer)).start()

Действительно, в самом теле функции test_with_barrier, метод wait() барьера применяется для синхронизации этих процессов:

synchronizer.wait()

Выполнив данный сценарий, мы можем видеть, что наши процессы p1 и p2 выводят, как и ожидалось, одно и то же значение временного штампа:

> python processes_barrier.py
process p4 - test_without_barrier ----> 2019-03-03 08:58:06.159882
process p3 - test_without_barrier ----> 2019-03-03 08:58:06.144257
process p1 - test_with_barrier ----> 2019-03-03 08:58:06.175505
process p2 - test_with_barrier ----> 2019-03-03 08:58:06.175505

Также ознакомьтесь...

Наша следующая схема показывает вам как работает барьер с двумя процессами:

Рисунок 3-1

Управление барьером для процессов

Дополнительно

Для знакомства с дополнительными примерами синхронизациями обратитесь к этому блогу.

Применение пула процессов

Механизм пула процессов делает возможным параллельное выполнение некой функции на множестве входных значений, распределяя имеющиеся данные между процессами. Сам пул процессов, таким образом, позволяет реализовывать так называемый параллелизм данных, который основывается на распределении данных по различным процессам, которые параллельно работают с данными.

Приготовление

Обсуждаемая нами библиотека multiprocessing предоставляет класс Pool для упрощения обработки параллельных задач.

Этот класс Pool обладает такими методами:

apply(): Блокирует до момента готовности получения результата.

span class="term">apply_async(): Это некий вариант метода apply(), который возвращает некий получаемый в результате объект. Это некая асинхронная операция, которая не будет блокировать свой основной поток до тех пор, пока не выполнятся все классы потомков.

map(): Это некий параллельный вариант встроенной функции map(). Он осуществляет блокирование до тех пор, пока не получен конечный результат, а также он расчленяет все итеративные данные на некое число фрагментов, которые подставляются его пулу процессов как отдельные задачи.

map_async(): Это некий вариант метода map(), который возвращает объект result. Когда определён некий обратный вызов, тогда он должен быть выполнен, причём с приёмом некого отдельного аргумента. После того как получаемый результат становится готовым, к нему применяется этот обратный вызов (только если такой вызов не приводит к отказу). Обратный вызов должен быть выполнен немедленно; в противном случае тот поток, который обрабатывает данный результат будет заблокирован.

Как это сделать...

Данный пример покажет вам как реализовать некий пул процессов для выполнения параллельного приложения. Мы создаём некий пул из четырёх процессов, а затем мы применяем метод map пула для выполнения какой- то простой функции:

Импортируем библиотеку multiprocessing:

import multiprocessing

Наш метод Pool применяет function_square для введённого элемента чтобы выполнить некое простое вычисление:

def function_square(data):
    result = data*data
    return result

if __name__ == '__main__':

Следующий параметр вводит некий список целых от 0 до 100:

inputs = list(range(0,100))

Общее число параллельных процессов равняется 4:

pool = multiprocessing.Pool(processes=4)

Наш метод pool.map подставляется в пул процессов как отдельные задачи:

pool_outputs = pool.map(function_square, inputs)
pool.close()
pool.join()

Полученные результаты вычислений сохраняются в pool_outputs:

print ('Pool    :', pool_outputs)

Важно отметить, что сам получаемый результат метода pool.map() эквивалентен встроенной функции Python map(), за исключением того что в нашем случае процессы выполняются параллельно.

Как это работает...

Здесь, воспользовавшись приводимым ниже оператором, мы создали пул из четырёх процессов:

pool = multiprocessing.Pool(processes=4)

Каждый процесс имеет некий список целых в качестве данных на входе. Здесь, pool.map работает точно так же как и стандартный map, но применяет множество процессов, число которых, четыре, было ранее определено в процессе создания пула:

pool_outputs = pool.map(function_square, inputs)

Для завершения вычислений в данном пуле применяются обычные функции close и join:

pool.close()
pool.join()

Для выполнения этого наберите такую команду:

> python process_pool.py

Вот полученные нами результаты по завершению наших вычислений:

Pool : [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801]

Также ознакомьтесь...

В своём предыдущем примере мы увидели, что Pool также предоставляет метод map, который позволяет нам применять некую функцию для различных наборов данных. В частности, тот сценарий, в котором одни и те же действия выполняются параллельно над теми же самыми данными именуется параллелизмом данных.

В нашем следующем примере, в котором мы применяем Pool и map, мы создаём pool из 5 обработчиков и, применяя метод map, некая функция f применяется к списку из 10 элементов:

from multiprocessing import Pool

def f(x):
    return x+10

if __name__ == '__main__':
     p=Pool(processes=5)
     print(p.map(f, [1, 2, 3,5,6,7,8,9,10]))

Вывод получается таким:

11 12 13 14 15 16 17 18 19 20

Дополнительно

Для знакомства с дополнительными примерами пулов процессов обратитесь к следующей ссылке.

Глава 4. Обмен сообщениями

Содержание

Глава 4. Обмен сообщениями

Технические требования

Понимание структуры MPI

Применение модуля Python mpi4py

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Реализация взаимодействия точка- точка

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Обход проблем взаимной блокировки

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие через широковещание

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие при помощи функции разброса

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие при помощи функции получения

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Коллективное взаимодействие с применением Всех-ко-всем

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Операция свёртки

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Оптимизация взаимодействия

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

В этой главе мы кратко рассмотрим MPI (Message Passing Interface, Интерфейс передачи сообщений), который является спецификацией обмена сообщениями. Самая первейшая цель MPI состоит в установлении некого действенного, гибкого и переносимого стандарта взаимодействия обмена сообщениями.

В основном мы покажем те функции имеющейся библиотеки, которые содержат синхронные и асинхронные примитивы взаимодействия, такие как (отправка/ получение - send/receive) и (широковещательный/ все- ко- всем - broadcast/all-to-all), имеющиеся операции сочетания параллельных результатов вычислений (сбор/ понижение - gather/ reduce) и, наконец, примитивы синхронизации между процессами (барьеры).

Более того, основные функции контроля сетевого взаимодействия будут представлены путём определения имеющихся топологий.

В этой главе мы рассмотрим такеи рецепты:

Применение модуля mpi4py Pyhon

Реализация взаимодействия точка- точка

Избежание проблем взаимной блокировки

Совместное взаимодействие через широковещательный обмен

Совместное взаимодействие при помощи функции scatter

Совместное взаимодействие с применением функции gather

Совместное взаимодействие за счёт Alltoall

Операция редуцирования

Оптимизация взаимодействия

Технические требования

Для данной главы нам потребуются библитотеки mpich и mpi4py.

Библиотека mpich является переносимой реализацией MPI. Это свободно распространяемое программное обеспечение для различных версий Unix (включая Linux and macOS), а также для Microsoft Windows.

Для установки mpich воспользуйтесь тем установщиком, который выгружается с основной страницы выгрузки. Более того, убедитесь в правильности выбора между 32- битной и 64- битной версиями для получения правильной для вашей машины.

Модуль библиотеки Python mpi4py предоставляет привязки Python для стандарта MPI. Он реализуется поверх спецификации MPI-1/2/3 и выставляет некий API, который основывается на привязках MPI-2 C++.

Процесс установки mpi4py в машине Windows таков:

C:> pip install mpi4py

Пользователям Anaconda надо набрать следующее:

C:> conda install mpi4py

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

Это подразумевает, что та нотация, которая применятся для запуска примеров mpi4py будет следующей:

C:> mpiexec -n x python mpi4py_script_name.py

Команда mpiexec является обычным способом для запуска параллельных заданий: x является общим числом процессов для применения, в то время как mpi4py_script_name.py выступает в качестве названия исполняемого сценария.

Понимание структуры MPI

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

Мы будем придерживаться стандарта MPI через библиотеку Python mpi4py.

Вплоть до 1990-х, написание параллельных приложений для различных архитектур было намного более сложным заданием чем в наши дни. Сам процесс поддерживало множество библиотек, однако для них не существовало стандартного свособа. К этому времени большинство параллельных приложений предназначалось для сред научных исследований.

Та модель, которая в основном принималась всеми разнообразными библиотеками была моделью передачи сообщений, при которой само взаимодействие между процессами происходило через обмен сообщениями и без использования совместных ресурсов. Например, процесс хозяин мог назначать некое задание своим подчинённым просто отправляя некое сообщение, которое описывало подлежащую исполнению работу. Вторым, очень простым примером здесь является некое параллельное приложение, которое выполняет некую сортировку слиянием. Все сортируемые данные локальны для всех процессов, а получаемые результаты передаются прочим процессам, которые имеют дело с данным слиянием.

Так как все библиотеки в основном применяли одну и ту же модель, хотя и с незначительными отличиями друг от друга, основные авторы имевшихся разнообразных библиотек встретились в 1992 для определения некого стандартного интерфейса обмена сообщениями и, начиная с этого момента, был рождён MPI. Этот интерфейс позволил программистам писать переносимые параллельные приложения для большинства параллельных архитектур, причём применяя те же самые функциональные возможности и модели, кторые они уже использовали.

Изначально MPI был спроектирован для архитектур с распределённой памятью, которая начала свой рост популярности 20 лет тому назад:

Рисунок 4-1

Схема архитектуры с распределённой памятью

Со временем системы с распределённой памятью начали сочетаться друг с другом, создавая гибридные системы с распределённой/ разделяемой памятью:

Рисунок 4-2

Схема архитектуры гибридной системы

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

Сильные стороны MOI можно суммировать так:

Стандартизация: Она поддерживается всеми платформами HPC (High-Performance Computing, Высокопроизводительных вычислений).

Переносимость: Все применяемые к самому исходному коду изменения минимальны, что полезно если вы принимаете решение использовать данное приложение на различных платформах, которые также поддерживают этот стандарт.

Производительность: Производители могут создавать реализации, оптимизированные под определённые виды оборудования и получать наилучшую производительность.

Функциональность: В MPI-3 определяется более 440 процедур, однако многие программы могут быть написаны с использованием менее 10 процедур.

В своих последующих разделах мы изучим саму основную библиотеку Python для передачи сообщений: библиотеку mpi4py.

Применение модуля Python mpi4py

Язык программирования Python предоставляет различные модули MPI для написания параллельных программ. Наиболее интересной из них является библиотека mpi4py. Она построена поверх спецификации MPI-1/2 и предоставляет некий объектно- ориентированный интерфейс, который близко следует привязкам MPI-2 C++. Некий пользователь C MPI может применять этот модуль без изучения нового интерфейса. Таким образом, он широко применяется как почти полный пакте библиотеки MPI в Python.

Основными приложениями данного модуля, которые и описываются в данной главе, выступают следующие:

Взаимодействия точка- точка

Коллективные взаимодействия

Топологии

Как это сделать...

Давайте начнём наше путешествие по библиотеке MPI с изучения классического кода программы, который выводит на печать фразу Hello, world! во всех создаваемых экземплярах процессов:

Импортируем библиотеку mpi4py:

from mpi4py import MPI

[Замечание]
Замечание

В MPI те процессы, которые вовлечены в само выполнение параллельной программы идентифицируются некой последовательностью неотрицательных целых, называемых порядками (ranks).

У нас имеется некое число (p) процессов, в которых запущена некая программа, далее эти процессы имеют rank, который пробегает значения от 0 до p-1. В частности, чтобы получить значение порядка каждого процесса, мы должны воспользоваться функцией MPI COMM_WORLD. Эта функция имеет название communicator (средства связи), так как она определяет свой собственный набор всех процессов, которые могут совместно взаимодействовать:

comm = MPI.COMM_WORLD

Наконец, следующая функция Get_rank() возвращает rank (порядок) самого процесса при её вызове:

rank = comm.Get_rank()

После его вычисления, rank выводится на печать:

print ("hello world from process ", rank)

Как это работает...

В соответствии с имеющейся моделью исполнения MPI, наше приложение состоит из N (в данном примере 5) автономных процессов, причём каждый со своей собственной локальной памятью, способных взаимодействовать данными через выполнение обмена сообщениями.

Наше средство связи (коммуникатор) определяет некую группу процессов, которые могут взаимодействовать друг с другом. Применяемый здесь обработчик MPI.COMM_WORLD является устанавливаемым по умолчанию средством связи и содержит все процессы.

Значение идентификатора процесса основывается на порядках. Каждому процессу назначается некий порядок от каждого средства связи к которому он относится. Значением порядка является некое назначаемое ему целое, начинающееся с нуля и указывающее на все индивидуальные процессы в контексте определённого средства связи (коммуникатора). Распространённой практикой является определение того процесса, глобальный порядок которого 0, в качестве процесса хозяина. Через значение порядка (rank) разработчик может определять кто именно выступает процессом отправителем, а кто вместо этого являются процессами получателями.

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

Для выполнения данного кода наберите такую командную строку:

C:> mpiexec -n 5 python helloworld_MPI.py

Вот результат, полученный после выполнения данного кода (обратите внимание, что полученный порядок выполнения наших процессов не последовательный):

hello world from process  1
hello world from process  0
hello world from process  2
hello world from process  3
hello world from process  4

[Замечание]
Замечание

Следует отметить, что значение числа применяемых процессов строго зависит от характеристик той машины, на которой должна выполняться эта программа.

Также ознакомьтесь...

MPI относится к технологии программирования SPMD (Single Program Multiple Data, Одна программа со множеством данных).

SPMD это техника программирования, при которой все процессы выполняют одну и ту же программу, причём каждый для собственных данных. Основное отличие в исполнении между различными процессами происходит разделением самого потока данной программы на основе значения локального порядка (rank) данного процесса.

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

Дополнительно

Полное руководство по библиотеке mpi4py можно найти по ссылке.

Реализация взаимодействия точка- точка

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

Модуль Python mpi4py делает возможным взаимодействие точка- точка при помощи двух функций:

Comm.Send(data, process_destination): Эта функция отправляет данные в процесс назначения, идентифицируемый его порядком в имеющейся группе средства связи.

Comm.Recv(process_source): Эта функция получает данные от установленного процесса источника, который также идентифицируется его порядком в общей группе средств связи.

Значение параметра Comm, который является сокращением communicator (средств связи), задаёт ту группу процессов, которая может взаимодействовать через обмен сообщениями при помощи comm = MPI.COMM_WORLD.

Как это сделать...

В своём следующем примере мы применяем директивы comm.send и comm.recv для обмена сообщениями между различными процессами.

Импортируем относящуюся к делу библиотеку mpi4py:

from mpi4py import MPI

Затем мы определяем параметр средства связи, а именно comm, через оператор MPI.COMM_WORLD:

comm=MPI.COMM_WORLD

Значение параметра rank применяется для указания самого этого процесса:

rank = comm.rank

Будет полезно выводить на печать значение rank некого процесса:

print("my rank is : " , rank)

Затем мы начнём рассматривать значение порядка (rank) данного процесса. В данном случае для данного процесса rank равного 0, мы устанавливаем destination_process и data (в данном случае data = 10000000) для отправки:

if rank==0:
    data= 10000000
    destination_process = 4

После этого, применив оператор comm.send, установленные ранее данные отправляются в процесс получателя:

comm.send(data,dest=destination_process)
    print ("sending data %s " %data + \
           "to process %d" %destination_process)

rank 1 destination_process 8 "hello"

if rank==1:
    destination_process = 8
    data= "hello"
    comm.send(data,dest=destination_process)
    print ("sending data %s :" %data + \
           "to process %d" %destination_process)

Процесс ос значением rank равным 4 это процесс получателя. Действительно, значение параметра источника (то есть rank равный 0) устанавливается как параметр в соответствующем операторе comm.recv:

if rank==4:
    data=comm.recv(source=0)

Теперь, при помощи приводимого ниже кода полученные от процесса 0 данные следует отобразить:

print ("data received is = %s" %data)

Самый последний подлежащий установке процесс, это процесс с номером 9. Здесь мы задаём значением процесс источника с rank равным 1 как некий параметр в операторе comm.recv:

if rank==8:
    data1=comm.recv(source=1)

После этого на печать выводится data1:

print ("data1 received is = %s" %data1)

Как это работает...

Мы выполнили данный пример с общим числом процессов, равным 9. Поэтому в нашей группе средства связи comm у нас имелось девять задач, которые могли взаимодействовать друг с другом:

comm=MPI.COMM_WORLD

Кроме того, для идентификации некой задачи или процесса мы используем значение его rank:

rank = comm.rank

У нас имеются два отправляющих процесса и два получающих процесса. Наш процесс со значением rank равным 0, отправляет данные принимающему численные данные процессу с rank равным 4:

if rank==0:
    data= 10000000
    destination_process = 4
    comm.send(data,dest=destination_process)

Аналогично, нам следует определить значением получающего процесс со значением rank равным 4. Мы также обращаем внимание на то, что наш оператор comm.recv должен содержать некий аргумент, значение порядка своего процесса отправки:

if rank==4:
    data=comm.recv(source=0)

Для других процессов отправки и получения (процесс со значением rank, равным 1, и процесс со значением rank, равным 8, соответственно) ситуация та же самая, с единственным отличием в типе самих данных.

В данном случае для процесса отправителя у нас имеется на отправку некая строка:

if rank==1:
    destination_process = 8
    data= "hello"
    comm.send(data,dest=destination_process)

if rank==8:
    data1=comm.recv(source=1)

Наша следующая схема суммирует протокол взаимодействия точка- точка в mpi4py:

Рисунок 4-3

Схема обмена в протоколе отправки/ приёма

Как вы можете видеть, она описывает двухшаговый процесс, состоящий из отправки неких ДАННЫХ от одной задачи (отправителя, sender) другой задаче (получателю, receiver), получающей эти данные. Отправляющая задача обязана определить подлежащие отправке данные и своё пункт назначения (её процесс получатель), в то время как сама получающая задача обязана определить свой источник получаемого сообщения.

Для запуск этого сценария мы будем применять 9 процессов:

C:> mpiexec -n 9 python pointToPointCommunication.py

Вот тот вывод, который мы получили после запуска данного сценария:

my rank is : 7
my rank is : 5
my rank is : 2
my rank is : 6
my rank is : 3
my rank is : 1
sending data hello :to process 8
my rank is : 0
sending data 10000000 to process 4
my rank is : 4
data received is = 10000000
my rank is : 8
data1 received is = hello

Также ознакомьтесь...

Функции comm.send() и comm.recv() являются блокирующими функциями, что означает, что они блокируют своего вызывающего до тех пор, пока буферизованные данные, вовлекаемые в этот процесс, не будут безопасно использованы. Кроме того, в MPI имеются два метода управления отправки и получения сообщений:

Режим с буферизацией: Имеющийся поток управления возвращается в данную программу сразу после того как отправляемые данные скопированы в некий буфер. Это не означает что данное сообщение отправлено или получено.

Синхронный режим: Сама функция завершится когда соответствующая функция receive приступит к получению сообщения.

Дополнительно

Рекомендуем интересное руководство по данной теме.

Обход проблем взаимной блокировки

Общей проблемой, с которой мы сталкиваемся, является взаимная блокировка (deadlock). Это ситуация, при которой два (или более) процесса блокируют друг друга и ожидают от другой программы некого действия, которое обслуживает нечто иное и наоборот. Модуль mpi4py не предоставляет никакое особенной функциональности для разрешения проблемы взаимной блокировки, однако имеется ряд мер, которым стоит следовать разработчикам во избежание данной проблемы взаимного блокирования.

Как это сделать...

Давайте проанализируем приводимый ниже код Python, который представляет типичную проблему взаимной блокировки. У нас имеются два процесса - с rank равным 1 и с rank равным 5 - которые взаимодействуют друг с другом и оба имеют функции отправителя и получателя:

Импортируем библиотеку mpi4py:

from mpi4py import MPI

Опредеяем comm как средство связи и значение параметра rank:

comm=MPI.COMM_WORLD
rank = comm.rank
print("my rank is %i" % (rank))

Процесс со значением rank равным 1 отправляет и получает данные от процесса со значением rank равным 5:

if rank==1:
    data_send= "a"
    destination_process = 5
    source_process = 5
    data_received=comm.recv(source=source_process)
    comm.send(data_send,dest=destination_process)
    print ("sending data %s " %data_send + \
           "to process %d" %destination_process)
    print ("data received is = %s" %data_received)

Аналогичным образом мы определяем сой процесс со значением rank равным 5:

if rank==5:
    data_send= "b"

Значения процессов отправителя и получателя равны 1:

destination_process = 1
    source_process = 1
    comm.send(data_send,dest=destination_process)
    data_received=comm.recv(source=source_process)
    print ("sending data %s :" %data_send + \
           "to process %d" %destination_process)
    print ("data received is = %s" %data_received)

Как это работает...

Когда мы запускаем эту программу (имеет смысл запускать её только с двумя процессами), тогда мы замечаем, что ни один из наших двух процессов не может продолжить своё выполнение:

C:\> mpiexec -n 9 python deadLockProblems.py

my rank is : 8
my rank is : 6
my rank is : 7
my rank is : 2
my rank is : 4
my rank is : 3
my rank is : 0
my rank is : 1
sending data a to process 5
data received is = b
my rank is : 5
sending data b :to process 1
data received is = a

Оба процесса готовы принять сообщения от другой стороны и застряли в этом состоянии. Это происходит по причине того, что MPI функция comm.recv() и MPI функция comm.send() блокируют их. Это означает, что вызывавший её процесс ожидает завершения вызова. Что касается MPI comm.send(), её завершение произойдёт когда все данные были отправлены и могут быть перекрыты без изменения отправленного сообщения.

Соответствующее завершение MPI comm.recv() вместо этого происходит когда необходимые данные были получены и могут использоваться. Для решения данной проблемы самая первая идея состоит в замене MPI comm.recv() на comm.send() следующим образом:

if rank==1:
    data_send= "a"
    destination_process = 5
    source_process = 5
    comm.send(data_send,dest=destination_process)
    data_received=comm.recv(source=source_process)

    print ("sending data %s " %data_send + \
           "to process %d" %destination_process)
    print ("data received is = %s" %data_received)

if rank==5:
    data_send= "b"
    destination_process = 1
    source_process = 1
    data_received=comm.recv(source=source_process)
    comm.send(data_send,dest=destination_process)

    print ("sending data %s :" %data_send + \
           "to process %d" %destination_process)
    print ("data received is = %s" %data_received)

Это решение, даже если оно и верно, не гарантирует что мы избежим взаимной блокировки. На самом деле, взаимодействие осуществляется через буфер посредством инструкции comm.send().

MPI копирует подлежащие отправке данные. Этот режим работает без проблем, однако только когда имеющийся буфер способен удерживать их все. Если этого не происходит, тогда имеется взаимная блокировка: отправитель не способен завершить отправку своих данных по причине занятости общего буфера, а получатель не может получить данные, ибо он блокирован своим вызовом MPI comm.send(), который всё ещё не завершён.

Для данного случая подходящим решением, которое позволит нам избежать взаимного блокирования является применение замены местами функций отправки и приёма с тем, чтобы сделать их асимметричными:

if rank==1:
    data_send= "a"
    destination_process = 5
    source_process = 5
    comm.send(data_send,dest=destination_process)
    data_received=comm.recv(source=source_process)

if rank==5:
    data_send= "b"
    destination_process = 1
    source_process = 1
    comm.send(data_send,dest=destination_process)
    data_received=comm.recv(source=source_process)

Окончательно мы получаем правильный вывод:

C:\> mpiexec -n 9 python deadLockProblems.py

my rank is : 4
my rank is : 0
my rank is : 3
my rank is : 8
my rank is : 6
my rank is : 7
my rank is : 2
my rank is : 1
sending data a to process 5
data received is = b
my rank is : 5
sending data b :to process 1
data received is = a

Также ознакомьтесь...

Предложенное для данной взаимной блокировки решение не является единственным.

Существует, например, некая функция, которая унифицирует единый вызов, который отправляет какое- то сообщение заданному процессу и поучает другое сообщение, которое поступает от иного процесса. Такая функция имеет название Sendrecv:

Sendrecv(self, sendbuf, int dest=0, int sendtag=0, recvbuf=None, int source=0, int recvtag=0, Status status=None)

Как вы можете видеть, необходимые ей параметры те же самые, что и у MPI comm.send() с comm.recv() (в данном случае эта функция также блокирующая). Однако Sendrecv предлагает существенное преимущество того, что оставляет подсистему взаимодействия ответственной за проверку имеющихся зависимостей между отправителями и получателями, тем самым избегая взаимной блокировок.

В таком случае наш код из предыдущего примера преобразуется в такой:

if rank==1:
    data_send= "a"
    destination_process = 5
    source_process = 5
    data_received=comm.sendrecv(data_send,dest=\
                                destination_process,\
                                source =source_process)
if rank==5:
    data_send= "b"
    destination_process = 1
    source_process = 1
    data_received=comm.sendrecv(data_send,dest=\
                                destination_process,\
                                source=source_process)

Дополнительно

Некий забавный анализ относительно того насколько параллельное программирование сложно по причине управления взаимными блокировками можно найти по следующей ссылке.

{Прим. пер.: обращаем также ваше внимание на наши переводы Полного руководство параллельного программирования на Python Куан Нгуена и Asyncio в Python 3 Цалеба Хаттингха.}

Коллективное взаимодействие через широковещание

В процессе разработки параллельного кода мы часто обнаруживаем себя в ситуации, при которой мы должны должны разделять, причём для множества процессов, конкретное значение определённой переменной во время исполнения или заданной операции для переменных, производимых всеми процессами (возможно с различными значениями).

Для разрешения подобных случаев применяются деревья взаимодействия (например, процесс 0 отправляет данные в процессы 1 и 2, которые, в свою очередь, заботятся об отправке их в процессы 3,4,5,6 и так далее).

Вместо этого MPI предоставляет функции, которые идеальны для такого обмена информацией или использования множества процессов, которые чётко оптимизированы под те машины, в которых они выполняются:

Рисунок 4-4

Широковещательная отправка данных процессом 0 в процессы 1, 2, 3 и 4

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

Приготовление

Функциональность широковещания mpi4py предоставляется таким методом:

buf = comm.bcast(data_to_share, rank_of_root_process)

Эта функция отправляет те сведения, которые содержатся в сообщении корневого процесса всем остальным процессам, которые относятся к средству связи (коммуникатору) comm.

Как это сделать...

Давайте теперь рассмотрим пример, в котором мы воспользуемся функцией broadcast. У нас имеется некий корневой процесс со значением rank равным 0, который разделяет свои собственные данные, variable_to_share, со всеми прочими процессами, заданными в установленной группе средства связи:

Давайте импортируем необходимую библиотеку mpi4py:

from mpi4py import MPI

Теперь давайте определим общее средство связи и значение параметра rank:

comm = MPI.COMM_WORLD
rank = comm.Get_rank()

Когда дело касается процесса, rank которого равен 0, мы задаём значение переменной, подлежащей совместному использованию всеми прочими процессами:

if rank == 0:
    variable_to_share = 100
else:
    variable_to_share = None

Наконец, мы задаём широковещательную отправку, имея для его root процесс со значением rank равным нулю:

	variable_to_share = comm.bcast(variable_to_share, root=0)
print("process = %d" %rank + " variable shared  = %d " \
                               %variable_to_share)

Как это работает...

Наш корневой процесс со значением rank равным 0 создаёт экземпляр переменной variable_to_share, который равен 100. Эта переменная будет разделяться со всеми прочими процессами в установленной группе средства взаимодействия:

if rank == 0:
   variable_to_share = 100

Для осуществления этого мы также вводим некий оператор широковещательного взаимодействия:

variable_to_share = comm.bcast(variable_to_share, root=0)

В данном случае значения параметров для нашей функции таковы:

Сами подлежащие совместному использованию данные (variable_to_share).

Значение корневого процесса, которым выступает процесс со значением порядка, равным нулю (root=0).

Запустив этот код, мы получили группу средства связи из 10 процессов и разделяемую между всеми прочими процессами в этой группе переменную variable_to_share. Наконец, наш оператор print визуализирует значения порядка выполненных процессов и само значение его переменной:

print("process = %d" %rank + " variable shared  = %d " \
                     %variable_to_share)

После установки 10 процессов получаемый вывод таков:

C:\> mpiexec -n 10 python broadcast.py
process = 0
variable shared = 100
process = 8
variable shared = 100
process = 2 variable
shared = 100
process = 3
variable shared = 100
process = 4
variable shared = 100
process = 5
variable shared = 100
process = 9
variable shared = 100
process = 6
variable shared = 100
process = 1
variable shared = 100
process = 7
variable shared = 100

Также ознакомьтесь...

Коллективное взаимодействие делает возможным одновременную передачу данных между множеством процессов в некой группе. Имеющаяся библиотека mpi4py предоставляет коллективное взаимодействие, но только в версии с блокировкой (то есть она блокирует вызывающий метод до тех пор, пока вовлечённые в процесс буферируемые данные не смогут применяться безопасно).

Наиболее распространёнными операциями коллективного взаимодействия являются такие:

Барьерная синхронизация для определённой группы процессов

Функции взаимодействия:

Широковещательная доставка данных от одного процесса всем процессам в установленной группе

Накопление (Gathering) данных от всех процессов одним процессом

Рассеивание (Scattering) данных от одного процесса всем процессам

Операции редукции

Дополнительно

Для обращения к полному введению в Python и MPI отсылаем вас к следующей ссылке.

Коллективное взаимодействие при помощи функции разброса

Функциональность разброса (scatter) очень схожа с широковещательным взаимодействием за одним важным исключением: в то время как comm.bcast отсылает одни и те же данные всем ожидающим процессам, comm.scatter способен отсылать фрагменты данных из массива различным процессам.

Приводимая далее схема иллюстрирует данную функциональность разбрасывания:

Рисунок 4-5

Разброс данных от процесса 0 в процессы 1, 2, 3 и 4

Данная функция comm.scatter получает элементы заданного массива и распределяет их по процессам в соответствии с их порядковыми номерами, для которых самый первым элемент отправляется в процесс 0, второй элемент в процесс 1, и так далее. Реализующая это функция из mpi4py такова:

recvbuf  = comm.scatter(sendbuf, rank_of_root_process)

Как это сделать...

В своём следующем примере мы рассмотрим как распределять данные в различные процессы при помощи функциональности scatter:

Импортируем нужную нам библиотеку mpi4py:

from mpi4py import MPI

Далее задаём как обычно параметры comm и rank:

comm = MPI.COMM_WORLD
rank = comm.Get_rank()

Для процесса со значением rank равным 0 будет разбрасываться следующий массив:

if rank == 0:
    array_to_share = [1, 2, 3, 4 ,5 ,6 ,7, 8 ,9 ,10]
else:
    array_to_share = None

После этого устанавливается recvbuf. Нашим процессом root является процесс со значением rank равным 0.

recvbuf = comm.scatter(array_to_share, root=0)
print("process = %d" %rank + " recvbuf = %d " %recvbuf)

Как это работает...

Наш процесс со значением rank равным 0 распределяет значения структуры данных array_to_share во все прочие процессы:

array_to_share = [1, 2, 3, 4 ,5 ,6 ,7, 8 ,9 ,10]

Параметр recvbuf указывает на значение iй переменной, которая будет отправляться в прочие процессы через соответствующий оператор comm.scatter:

recvbuf = comm.scatter(array_to_share, root=0)

Получаемый вывод таков:

C:\> mpiexec -n 10 python scatter.py
process = 0 variable shared  = 1
process = 4 variable shared  = 5
process = 6 variable shared  = 7
process = 2 variable shared  = 3
process = 5 variable shared  = 6
process = 3 variable shared  = 4
process = 7 variable shared  = 8
process = 1 variable shared  = 2
process = 8 variable shared  = 9
process = 9 variable shared  = 10

Мы также обращаем ваше внимание на одно из ограничений comm.scatter, которое состоит в том, что мы можем разбрасывать столько же элементов, сколько в выполняемом операторе имеется процессов. На самом деле, если вы попытаетесь разбросать больше элементов, чем определено процессов (в следующем примере три), тогда вы получите ошибку, подобную следующей:

C:\> mpiexec -n 3 python scatter.py
Traceback (most recent call last):
  File "scatter.py", line 13, in
    recvbuf = comm.scatter(array_to_share, root=0)
  File "Comm.pyx", line 874, in mpi4py.MPI.Comm.scatter
  (c:\users\utente\appdata\local\temp\pip-build-h14iaj\mpi4py\
  src\mpi4py.MPI.c:73400)
  File "pickled.pxi", line 658, in mpi4py.MPI.PyMPI_scatter
  (c:\users\utente\appdata\local\temp\pip-build-h14iaj\mpi4py\src\
  mpi4py.MPI.c:34035)
  File "pickled.pxi", line 129, in mpi4py.MPI._p_Pickle.dumpv
  (c:\users\utente\appdata\local\temp\pip-build-h14iaj\mpi4py
  \src\mpi4py.MPI.c:28325)
  ValueError: expecting 3 items, got 10
  mpiexec aborting job...

job aborted:
rank: node: exit code[: error message]
0: Utente-PC: 123: mpiexec aborting job
1: Utente-PC: 123

Также ознакомьтесь...

Библиотека mpi4py также предоставляет две другие функции, применяемые для разброса данных:

comm.scatter(sendbuf, recvbuf, root=0): Эта функция отправляет данные из одного процесса во все прочие из средства связи (коммуникатора).

comm.scatterv(sendbuf, recvbuf, root=0): Данная функция разбрасывает данные из одного процесса во все прочие в некой заданной группе, которая предоставляет различные количества данных и смещения на стороне отправки.

Аргументы sendbuf и recvbuf должны задаваться в терминах некого списка (как и в соответствующей функции точка- точка comm.send):

buf = [data, data_size, data_type]

Здесь данные должны быть подобным буферу объектом data с установленным размером data_size и значением типа данных data_type.

Дополнительно

Занятное руководство по широковещательному MPI представлено по следующей ссылке.

Коллективное взаимодействие при помощи функции получения

Функция gather выполняет обратное функции scatter действие. В этом случае все процессы отправляют данные процессу корня, который собирает получаемые данные.

Приготовление

Реализуемая mpi4py функция gather такова:

recvbuf = comm.gather(sendbuf, rank_of_root_process)

Здесь sendbuf являются подлежащими отправке данными, а rank_of_root_process представляет обработку получения всех этих данных:

Рисунок 4-6

Накопление данных от процессов 1, 2, 3 и 4

Как это сделать...

В нашем следующем примере мы представим то условие, которое представлено на предыдущей схеме, при котором каждый из процессов строит свои собственные данные, подлежащие отправке в установленный корневой процесс, который определяется значением rank равным нулю:

Наберите необходимый импорт:

from mpi4py import MPI

Затем мы определяем три следующих параметра. Параметр comm это наше средство связи, rank предоставляет значение порядка для данного процесса, а size это общее число имеющихся процессов:

comm = MPI.COMM_WORLD
size = comm.Get_size()
rank = comm.Get_rank()

Здесь мы определяем то значение, которое будет получено от процессора с соответствующим значением rank:

data = (rank+1)**2

Наконец, наш накопитель предоставляется через функцию comm.gather. Кроме того обратите внимание, что нашим корневым процессом (тот процесс, который будет накапливать данные от прочих) является процессом нулевым значением порядка:

data = comm.gather(data, root=0)

Для процесса со значением rank равным 0, накапливаются все отправляемые данные, которые и выводятся на печать:

if rank == 0:
    print ("rank = %s " %rank +\
          "...receiving data to other process")
   for i in range(1,size):
       value = data[i]
       print(" process %s receiving %s from process %s"\
            %(rank , value , i))

Как это работает...

Наш корневой процесс с порядком 0 получает данные от всех прочих четырёх процессов, как это и представлено на приводившейся ранее схеме.

Мы устанавливаем n (= 5) процессов отправляющих свои данные:

data = (rank+1)**2

Когда значение rank равно 0, тогда данные накапливаются в неком массиве:

if rank == 0:
    for i in range(1,size):
        value = data[i]

Вместо этого данные для накопления представлены такой функцией:

data = comm.gather(data, root=0)

Наконец, мы запускаем свой код, настроенный на группу с числом процессов, равным 5:

C:\> mpiexec -n 5 python gather.py
rank = 0 ...receiving data to other process
process 0 receiving 4 from process 1
process 0 receiving 9 from process 2
process 0 receiving 16 from process 3
process 0 receiving 25 from process 4

Также ознакомьтесь...

Для сбора данных mpi4py предоставляет такие функции:

Накопление в одной задаче: comm.Gather, comm.Gatherv и comm.gather.

Накопление во всех задачах: comm.Allgather, comm.Allgatherv и comm.allgather.

Дополнительно

Дополнительные сведения относительно mpi4py можно найти по ссылке.

Коллективное взаимодействие с применением Всех-ко-всем

Коллективное взаимодействие Alltoall сочетает в себе функциональности scatter и gather.

Как это сделать...

В своём следующем примере мы рассмотрим реализацию mpi4py для comm.Alltoallю Мы рассмотрим некую группу взаимодействующих процессов, в которой каждый процесс отправляет и получает некий массив численных данных от прочих определённых в его группе процессов:

Для данного примера импортируются относящиеся к делу библиотеки mpi4py и numpy:

from mpi4py import MPI
import numpy

Как и в нашем предыдущем примере, нам требуется тот же самый набор параметров comm, size и rank:

comm = MPI.COMM_WORLD
size = comm.Get_size()
rank = comm.Get_rank()

Таким образом, мы обязаны определить те данные, которые будут отправляться для каждого из процессов (senddata) и в то же самое время данные, получаемые их прочих процессов (recvdata):

senddata = (rank+1)*numpy.arange(size,dtype=int)
recvdata = numpy.empty(size,dtype=int)

Наконец, выполняется функция Alltoall:

comm.Alltoall(senddata,recvdata)

Отображаются все отосланные и принятые данные для всех процессов:

print(" process %s sending %s receiving %s"\
      %(rank , senddata , recvdata))

Как это работает...

Метод comm.alltoall получает значения iх объектов из соответствующего аргумента sendbuf задачи j и копирует его в свой j объект значения аргумента recvbuf задачи i.

Когда мы запустим этот код с некой группой системы связи 5 процессов, тогда наш вывод будет следующим:

C:\> mpiexec -n 5 python alltoall.py
process 0 sending [0 1 2 3 4] receiving [0 0 0 0 0]
process 1 sending [0 2 4 6 8] receiving [1 2 3 4 5]
process 2 sending [ 0 3 6 9 12] receiving [ 2 4 6 8 10]
process 3 sending [ 0 4 8 12 16] receiving [ 3 6 9 12 15]
process 4 sending [ 0 5 10 15 20] receiving [ 4 8 12 16 20]

Мы также можем выразить то что происходит при помощи такой схемы:

Рисунок 4-7

Коллективное взаимодействие Alltoall

Наши относящиеся к этой схеме наблюдения таковы:

Наш процесс P0 содержит массив данных [0 1 2 3 4], в котором он присваивает 0 самому себе, 1 процессу P1, 2 процессу P2, 3 процессу P3 и 4 процессу P4.

Наш процесс P1 содержит массив данных [0 2 4 6 8], в котором он присваивает 0 процессу P0, 2 самому себе, 4 процессу P2, 6 процессу P3 и 8 процессу P4.

Наш процесс P2 содержит массив данных [0 3 6 9 12], в котором он присваивает 0 процессу P0, 3 процессу P1, 6 самому себе, 9 процессу P3 и 4 процессу P4.

Наш процесс P3 содержит массив данных [0 4 8 12 16], в котором он присваивает 0 процессу P0, 4 процессу P1, 8 процессу P2, 12 самому себе и 16 процессу P4.

Наш процесс P4 содержит массив данных [0 5 10 15 20], в котором он присваивает 0 процессу P0, 5 процессу P1, 10 процессу P2, 15 процессу P3 и 20 самому себе.

Также ознакомьтесь...

Alltoall воплощает взаимодействие, которое также называется всеобщим обменом. Эта операция применяется в разнообразных параллельных алгоритмах, таких как быстрое преобразование Фурье, транспонирование матриц, быстрая сортировка и некоторые операции соединения параллельных баз данных.

В mpi4py имеются три типа коллективного взаимодействия Alltoall:

comm.Alltoall(sendbuf, recvbuf): Рассеяние/ набор данных Alltoall для всех- ко всем процессов в некой группе.

comm.Alltoallv(sendbuf, recvbuf): Векторные рассеяние/ набор данных Alltoall для всех- ко всем процессов в некой группе, предоставляющее различные количества данных и смещений.

comm.Alltoallw(sendbuf, recvbuf): Обобщённое взаимодействие Alltoall, допускающее различные числа, смещения и типы данных для каждого из партнёров.

Дополнительно

Некий забавный анализ модулей MPI Python можно выгрузить по следующей ссылке.

Операция свёртки

Аналогично comm.gather, comm.reduce получает некий массив элементов на входе в каждом из процессов и возвращает некий массив элементов вывода в свой корневой процесс. Получаемые элементы вывода сдержат результат свёртки.

Приготовление

В mpi4py мы определяем операции свёртки посредством такого оператора:

comm.Reduce(sendbuf, recvbuf, rank_of_root_process, op = type_of_reduction_operation)

Вам следует обратить внимание на то, что основное отличие от оператора comm.gather заключается в наличии параметра op, который является тем оператором, который вы бы желали применить к своим данным {для понижения порядка}, а модуль mpi4py содержит некий набор операций свёртки, которые можно применять.

Как это сделать...

Теперь мы увидим как реализовывать суммирование некого массива элементов при помощи свёртки MPI.SUM с использованием функциональности понижения порядка (reduction). Все процессы будут манипулировать неким массивом с размеров 10.

Для манипуляций с массивом мы пользуемся теми функциями, которые предоставляются модулем Python numpy:

Здесь импортируются относящиеся к делу библиотеки mpi4py и numpy:

import numpy
from mpi4py import MPI

Определяем параметры comm, size и rank:

comm = MPI.COMM_WORLD
size = comm.size
rank = comm.rank

Далее устанавливается значение оазмера нашего массива (array_size):

array_size = 10

Задаются значения данных для отправки и получения:

recvdata = numpy.zeros(array_size,dtype=numpy.int)
senddata = (rank+1)*numpy.arange(array_size,dtype=numpy.int)

Выводятся на печать значения процесса отправителя и сами отправляемые данные:

print(" process %s sending %s " %(rank , senddata))

Наконец выполняется сама операция Reduce. Обратите внимание, что значение процесса root устанавливается в 0, а параметр op устанавливается в значение MPI.SUM:

comm.Reduce(senddata,recvdata,root=0,op=MPI.SUM)

Затем отображается получаемый вывод операции свёртки следующим образом:

print ('on task',rank,'after Reduce:    data = ',recvdata)

Как это работает...

Для выполнения свёртки суммой мы применяем оператор comm.Reduce. Кроме того, мы указываем при помощи нулевого rank кто выступает процессом root, который и будет содержать recvbuf, представляющий окончательный результат нашего вычисления:

print ('on task',rank,'after Reduce:    data = ',recvdata)

Существенно что мы запускаем свой код с некой группой системы звязи из 10 процессов, так как именно это размер нашего массива обработки.

Появляющийся вывод выглядит так:

C:\> mpiexec -n 10 python reduction.py
  process 1 sending [ 0 2 4 6 8 10 12 14 16 18]
on task 1 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 5 sending [ 0 6 12 18 24 30 36 42 48 54]
on task 5 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 7 sending [ 0 8 16 24 32 40 48 56 64 72]
on task 7 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 3 sending [ 0 4 8 12 16 20 24 28 32 36]
on task 3 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 9 sending [ 0 10 20 30 40 50 60 70 80 90]
on task 9 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 6 sending [ 0 7 14 21 28 35 42 49 56 63]
on task 6 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 2 sending [ 0 3 6 9 12 15 18 21 24 27]
on task 2 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 8 sending [ 0 9 18 27 36 45 54 63 72 81]
on task 8 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 4 sending [ 0 5 10 15 20 25 30 35 40 45]
on task 4 after Reduce: data = [0 0 0 0 0 0 0 0 0 0]
 process 0 sending [0 1 2 3 4 5 6 7 8 9]
on task 0 after Reduce: data = [ 0 55 110 165 220 275 330 385 440 495]

Также ознакомьтесь...

Обратите внимание, что с помощью параметра op=MPI.SUM мы применяем значение операции суммы для всех элементов всоего массива колонок. Чтобы лучше разобраться с тем как работает сама свёртка, посмотрите на следующую схему:

Рисунок 4-8

Свёртка при коллективном взаимодействии

Наши операции отправки таковы:

Процесс P0 отправляет значения [0 1 2] массива данных.

Процесс P1 отправляет значения [0 2 4] массива данных.

Процесс P2 отправляет значения [0 3 6] массива данных.

наша операция свёртки суммирует iе элементы всех задач, а затем помещает полученные результаты в iе элементы в корневом процессе P0. Для самой операции получения, наш процесс P0 получает массив данных [0 6 12].

Вот некоторые операции свёртки, определяемые MPI:

op=MPI.MAX: Возвращает максимальное значение элемента.

op=MPI.MIN: Возвращает минимальное значение элемента.

op=MPI.SUM: Это сумма всех элементов.

op=MPI.PROD: Это произведение всех элементов.

op=MPI.LAND: Выполняет логическую операцию И по всем элементам.

op=MPI.MAXLOC: Возвращает максимальное значение и значение порядка того процесса, который её содержит.

op=MPI.MINLOC: Возвращает минимальное значение и значение порядка того процесса, который её содержит.

Дополнительно

По ссылке вы можете найти хорошее руководство по данной теме и много чего ещё.

Оптимизация взаимодействия

Неким интересным свойством предоставляемым MPI является относящаяся к виртуальным топологиям функциональность. Как уже отмечалось, все функции взаимодействия (точка- точка или коллективные) относятся к некой группе процессов. Мы уже применяли группу MPI_COMM_WORLD, которая содержит все процессы. Она назначает некий порядок от 0 до n-1 для каждого из процессов, которые относятся к системе связи с размером n.

Однако MPI позволяет нам назначать некой системе связи (коммуникатору) какую- то виртуальную топологию. Она задаёт некое назначение меток различным процессам: путём построения виртуальной топологии, каждый узел способен взаимодействовать только со своим виртуальным соседом, причём улучшая производительность, так как это снижает время исполнения.

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

MPI предоставляет две топологии построения. Первая конструкция создаёт Декартову топологии , в то время как последняя создаёт любой вид топологий. В частности, во втором случае мы должны предоставлять матрицу смежности своего графа для того что вы желаете построить. Мы будем иметь дело только с Декартовой топологией, посредством которой можно построить различные широко применяемые структуры, такие как сетка, кольцо и тороид.

Та функция mpi4py, которая применяется для создания Декартовой топологии выглядит так:

comm.Create_cart((number_of_rows,number_of_columns))

Здесь number_of_rows и number_of_columns определяют число строк и колонок получаемой в результате сетки.

Как это сделать...

В своём следующем примере мы покажем как реализовать Декартову топологию с размером M×N. Кроме того мы определим некий набор координат для понимания того как распологаются наши процессы:

Импортируем относящиеся к делу библиотеки:

from mpi4py import MPI
import numpy as np

Для продвижения по нашей топологии определяем следующие параметры:

UP = 0
DOWN = 1
LEFT = 2
RIGHT = 3

Для каждого из процессов наш следующий массив определяет его соседние процессы:

neighbour_processes = [0,0,0,0]

В своей программе main мы далее определяем параметры comm.rank и size:

if __name__ == "__main__":
    comm = MPI.COMM_WORLD
    rank = comm.rank
    size = comm.size

Теперь давайте построим необходимую топологию:

grid_rows = int(np.floor(np.sqrt(comm.size)))
grid_column = comm.size // grid_rows

Следующие приводимые условия гарантируют что наши процессы всегда находятся внутри этой топологии:

if grid_rows*grid_column > size:
    grid_column -= 1
if grid_rows*grid_column > size:
    grid_rows -= 1

Процес со значением rank равным 0 запускает построение этой топологии:

if (rank == 0) :
        print("Building a %d x %d grid topology:"\
              % (grid_rows, grid_column) )

    cartesian_communicator = \
                           comm.Create_cart( \
                               (grid_rows, grid_column), \
                               periods=(False, False), \
                               reorder=True)
    my_mpi_row, my_mpi_col = \
                cartesian_communicator.Get_coords\
                ( cartesian_communicator.rank )

    neighbour_processes[UP], neighbour_processes[DOWN]\
                             = cartesian_communicator.Shift(0, 1)
    neighbour_processes[LEFT],  \
                               neighbour_processes[RIGHT]  = \
                               cartesian_communicator.Shift(1, 1)
    print ("Process = %s
    \row = %s\n \
    column = %s ----> neighbour_processes[UP] = %s \
    neighbour_processes[DOWN] = %s \
    neighbour_processes[LEFT] =%s neighbour_processes[RIGHT]=%s" \
             %(rank, my_mpi_row, \
             my_mpi_col,neighbour_processes[UP], \
             neighbour_processes[DOWN], \
             neighbour_processes[LEFT] , \
             neighbour_processes[RIGHT]))

Как это работает...

Для каждого из процессов получаемый вывод должен читаться следующим образом: когда neighbour_processes = -1, имеется топологическая близость, в противном случае neighbour_processes отображает значение порядка близости данного процесса.

Получаемой в результате топологией является сетка 2×2 (отсылаем вас к приводимой далее схеме для представления сетки), значение размера которой равно числу процессов на входе; то есть, четырём:

grid_row = int(np.floor(np.sqrt(comm.size)))
grid_column = comm.size // grid_row
if grid_row*grid_column > size:
    grid_column -= 1
if grid_row*grid_column > size:
    grid_rows -= 1

Далее Декартова топология строится при помощи функции comm.Create_cart (обратите также внимание на значение параметра periods = (False,False)):

cartesian_communicator = comm.Create_cart( \
    (grid_row, grid_column), periods=(False, False), reorder=True)

Для того чтобы определять положение своего процесса мы применяем метод Get_coords() в следующем виде:

my_mpi_row, my_mpi_col =\
                cartesian_communicator.Get_coords(cartesian_communicator.rank )

Для самих процессов, дополнительно к поучению их координат мы должны вычислять и определять какой из процессов является ближайшим топологически. Для этой цели мы применяем функцию comm.Shift(rank_source,rank_dest):

neighbour_processes[UP], neighbour_processes[DOWN] =\
                                  cartesian_communicator.Shift(0, 1)

neighbour_processes[LEFT],  neighbour_processes[RIGHT] = \
                                    cartesian_communicator.Shift(1, 1)

Получаемая топология выглядит следующим образом:

Рисунок 4-9

Виртуальная сетка топологии 2 x 2

Как показывает данная схема, наш процесс P0 сцеплен с процессами P1 (RIGHT) и P2 DOWN). Процесс P1 сцеплен с процессами P3 (DOWN) и с P0 (LEFT. Процесс P3 сцеплен с процессами P1 (UP и P2 (LEFT. А процесс P2 сцеплен с процессами P3 (RIGHT и P0 (UP.

Наконец, выполнив этот сценарий, мы получаем такой результат:

C:\>mpiexec -n 4 python virtualTopology.py
Building a 2 x 2 grid topology:
Process = 0 row = 0 column = 0
 ---->
neighbour_processes[UP] = -1
neighbour_processes[DOWN] = 2
neighbour_processes[LEFT] =-1
neighbour_processes[RIGHT]=1

Process = 2 row = 1 column = 0
 ---->
neighbour_processes[UP] = 0
neighbour_processes[DOWN] = -1
neighbour_processes[LEFT] =-1
neighbour_processes[RIGHT]=3

Process = 1 row = 0 column = 1
 ---->
neighbour_processes[UP] = -1
neighbour_processes[DOWN] = 3
neighbour_processes[LEFT] =0
neighbour_processes[RIGHT]=-1

Process = 3 row = 1 column = 1
 ---->
neighbour_processes[UP] = 1
neighbour_processes[DOWN] = -1
neighbour_processes[LEFT] =2
neighbour_processes[RIGHT]=-1

Также ознакомьтесь...

Для получения тороидальной топологии с размером M×N, давайте снова воспользуемся comm.Create_cart, но на этот раз давайте установим значение параметра periods в periods=(True,True):

cartesian_communicator = comm.Create_cart( (grid_row, grid_column),\
                                 periods=(True, True), reorder=True)

Получается такой результат:

C:\> mpiexec -n 4 python virtualTopology.py
Process = 3 row = 1 column = 1
---->
neighbour_processes[UP] = 1
neighbour_processes[DOWN] = 1
neighbour_processes[LEFT] =2
neighbour_processes[RIGHT]=2

Process = 1 row = 0 column = 1
---->
neighbour_processes[UP] = 3
neighbour_processes[DOWN] = 3
neighbour_processes[LEFT] =0
neighbour_processes[RIGHT]=0

Building a 2 x 2 grid topology:
Process = 0 row = 0 column = 0
---->
neighbour_processes[UP] = 2
neighbour_processes[DOWN] = 2
neighbour_processes[LEFT] =1
neighbour_processes[RIGHT]=1

Process = 2 row = 1 column = 0
---->
neighbour_processes[UP] = 0
neighbour_processes[DOWN] = 0
neighbour_processes[LEFT] =3
neighbour_processes[RIGHT]=3

Полученный вывод описывает представленную здесь топологию:

Рисунок 4-10

Виртуальная тороидальная топология 2 x 2

Эта представленная на предыдущей схеме топология указывает, что процесс P0 сцеплен с процессами P1(RIGHT и LEFT) и P2(UP и DOWN), процесс P1 сцеплен с процессами P3(UP и DOWN) и P2(RIGHT и LEFT), а процесс P2 сцеплен с процессами P3(RIGHT и LEFT) и P0(UP и DOWN).

Дополнительно

Дополнительные сведения по MPI можно найти по следующей ссылке.

Глава 5. Асинхронное программирование

Содержание

Глава 5. Асинхронное программирование

Применение модуля Python concurrent.futures

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Управление основным циклом событий через asyncio

Понимание циклов событий

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Обработка сопрограммам при помощи asyncio

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Манипулирование задачами при помощи asyncio

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Работа с asyncio и фьючерсы

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Помимо обсуждавшихся последовательной и параллельной моделей исполнения имеется некая третья, и именно она фундаментально важна совместно с понятием программирования событий: асинхронная модель.

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

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

Это остаётся вне поля зрения пишущего код, в отличии от асинхронной модели. Исполнение или прекращение некой здачи продолжается тогда, когда оно требуется в явном виде.

Самое основное свойство данного вида программирования состоит в том, что ваш код не выполняется во множестве потоков, как в некой классической одновременной программе, а в неком отдельном потоке. Таким образом, не всегда будет истинно что они выполняются почти в одно и то же время.

В частности, мы обсудим модуль Python asyncio, который был предложен в Python 3.4. Он делает для нас возможным применение сопрограмм (coroutines) и фьючерсов (futures) для упрощения написания асинхронного кода и делает его намного более читаемым.

В этой главе мы обсудим следующие рецепты:

Применение модуля Python concurrent.futures

Управление основным циклом событий при помощи asyncio

Обработка сопрограмм посредством asyncio

Манипуляции с задачами через asyncio

Работа с asyncio и фьючерсами

Применение модуля Python concurrent.futures

Модуль concurrent.futures, который является частью стандартной библиотеки Python, предоставляет некий уровень абстракции поверх потоков путём моделирования их в качестве асинхронных функций.

Этот модуль строится на основе двух основных классов:

concurrent.futures.Executor: Это некий класс абстракции, который предоставляет методы для асинхронного исполнения вызовов.

concurrent.futures.Future: Инкапсулирует некое доступное к вызову исполнение. Объекты фьючерсов создают экземпляры посредством представление задач (функции с необязательными параметрами) для Executor. {Прим. пер.: мне нравится в качестве синонима слово "местоблюститель", некий объект, создаваемый под последующее размещение в нём какого- то исполнителя в виде размещаемой позже ссылки на функцию, возможно без параметров. Спейсмейкер под мировой рекорд в марафоне Элиуда Кипчоге.}

Вот некоторые основные методы этого модуля:

submit(function,argument): Планирует своими аргументами само выполнение способной к вызову функции {подстановка}.

map(function,argument): Выполняет получаемые аргументами функции в неком асинхронном режиме {установка соответствия}.

shutdown(Wait=True): Выдаёт исполнителю (Executor) сигнал на освобождение всех ресурсов.

К необходимым исполнителям доступ осуществляется через их подклассы: ThreadPoolExecutor или ProcessPoolExecutor. По той причине, что размещение экземпляров потоков и процессов является требующей ресурсов задачей, лучше помещать такие ресурсы в некий пул и пользоваться ими в качестве повторных запусков или исполнителей (что и определяет само понятие Executor) для параллельных или одновременных задач.

Сам применяемый нами здесь подход предусматривает применение некого пула исполнителей. Мы будем помещать необходимые средства в свой пул (потоки и процессы) и получать фьючерсы, что в результате приводит к тому, что они будут нам доступны в будущем. Естественно, мы можем дождаться чтобы все фьючерсы принесли реальные результаты.

Некий пул потоков или процессов (также именуемый слиянием - pooling) обозначает некое программное обеспечение управления, которое используется для оптимизации и упрощения использования потоков и/ или процессов внутри некой программы. Через размещение в пулах мы можем подставлять необходимую задачу (или задачи) для их выполнения таким накопителем (pooler).

Такой пул снабжается некой внутренней очередью приостановленных задач и ряда потоков или процессов, которая и исполняет их. В пуле вторично применяется концепция периодичности: некий поток (или процесс) используется определённое число раз для различных задач в процессе своего жизненного цикла. Это снижает накладные расходы создания новых потоков или процессов и увеличивает общую производительность данной программы.

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

Приготовление

Модуль concurrent.futures предоставляет два подкласса {Прим. пер.: в случае появления новых, они могут быть сюда добавлены} в своём классе Executor, который асинхронно манипулирует неким пулом потоков и пулом процессов. Эти два подкласса следующие:

concurrent.futures.ThreadPoolExecutor(max_workers)

concurrent.futures.ProcessPoolExecutor(max_workers)

Значение параметра max_workers указывает максимальное число исполнителей для выполнения асинхронных вызовов.

Как это сделать...

Здесь приводится некий пример применения пула потоков и процессов, в котором мы будем сравнивать время исполнения с тем временем, которое требуется для последовательного выполнения.

Подлежащая выполнению задача такова: у нас имеется список из 10 элементов. Каждый элемент этого списка создан для подсчёта 100 000 000 (просто чтобы занять время), а после этого, самое последнее число умножается на iй элемент из данного списка. В частности, мы выполняем оценку для таких случаев:

Последовательное выполнение

Пул потоков с пятью исполнителями

Пул процессов с пятью исполнителями

Теперь давайте рассмотрим как это сделать:

Импортируем относящиеся к делу библиотеки:

import concurrent.futures
import time

Задаём список чисел от 1 до 10:

number_list = list(range(1, 11))

Наша функция count(number) перечисляет числа от 1 до 100000000, а затем возвращает произведение number × 100,000,000:

def count(number):
    for i in range(0,100000000):
        i += 1
    return i*number

Следующая функция evaluate(item) вычисляет нашу функцию count для значения параметра item. Она выводит на печать само значение item и получаемый count(item) результат:

def evaluate(item):
    result_item = count(item)
    print('Item %s, result %s' % (item, result_item))

В __main__ осуществляются последовательное выполнение, пул потоков и пул процессов:

if __name__ == '__main__':

Для нашего последовательного исполнения имеющаяся функция evaluate исполняется для каждого элемента из number_list. После этого выводится на печать время исполнения:

start_time = time.clock()
    for item in number_list:
        evaluate(item)
    print('Sequential Execution in %s seconds' % (time.clock() -\
        start_time))

Вне зависимости от того исполняются ли потоки или процессы, используется одно и то же число исполнителей (max_workers=5). Естественно, для обоих пулов отображается время выполнения:

start_time = time.clock()
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as\
    executor:
        for item in number_list:
            executor.submit(evaluate, item)
    print('Thread Pool Execution in %s seconds' % (time.clock() -\
        start_time))
    start_time = time.clock()
    with concurrent.futures.ProcessPoolExecutor(max_workers=5) as\
    executor:
        for item in number_list:
            executor.submit(evaluate, item)
    print('Process Pool Execution in %s seconds' % (time.clock() -\
        start_time))

Как это работает...

Мы строим некий список чисел, хранимый в number_list:

number_list = list(range(1, 11))

Для каждого элемента из этого списка мы выполняем свою процедуру перечисления пока не достигнем 100000000 итераций, а затем умножаем полученное число на 100000000:

def count(number) :
    for i in range(0, 100000000):
        i=i+1
    return i*number

def evaluate_item(x):
    result_item = count(x)

В своей программе main мы выполняем ту же самую задачу последовательно:

if __name__ == "__main__":
   for item in number_list:
       evaluate_item(item)

Затем в параллельном режиме применяем возможности слития в пул concurrent.futures для некого пула потоков:

with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    for item in number_list:
        executor.submit(evaluate, item)

И то же самое для пула процесов:

with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
    for item in number_list:
        executor.submit(evaluate, item)

[Совет]
Совет

Обратите внимание, что и пул потоков и пул процессов настроены с max_workers=5; более того, когда max_workers равен None, он будет определяться общим числом процессоров в вашей машине.

Для выполнения этой программы откройте приглашение Командной строки и в той же самой папке, где находится данный пример, наберите следующее:

> python concurrent_futures_pooling.py

Выполнив свой предыдущий пример мы можем наблюдать исполнение трёх моделей работы с сопоставлением времён:

Item 1, result 10000000
Item 2, result 20000000
Item 3, result 30000000
Item 4, result 40000000
Item 5, result 50000000
Item 6, result 60000000
Item 7, result 70000000
Item 8, result 80000000
Item 9, result 90000000
Item 10, result 100000000
Sequential Execution in 6.8109448 seconds
Item 2, result 20000000
Item 1, result 10000000
Item 4, result 40000000
Item 5, result 50000000
Item 3, result 30000000
Item 8, result 80000000
Item 7, result 70000000
Item 6, result 60000000
Item 10, result 100000000
Item 9, result 90000000
Thread Pool Execution in 6.805766899999999 seconds
Item 1, result 10000000
Item 4, result 40000000
Item 2, result 20000000
Item 3, result 30000000
Item 5, result 50000000
Item 6, result 60000000
Item 7, result 70000000
Item 9, result 90000000
Item 8, result 80000000
Item 10, result 100000000
Process Pool Execution in 4.166398899999999 seconds

Следует отметить, что хотя наш пример и не затратен в плане вычислений, последовательное исполнение и выполнение пула потоков сопоставимы по времени. Наивысший результат времени выполнения даёт пул процессов.

Этот пул распределяет все процессы (в нашем случае пять процессов) между всеми доступными ядрами (для примера этой машины мы применяли четыре ядра) в режиме FIFO (first in, first out, Первый пришёл, первым обработан).

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

Значения времён исполнения, которые меньше в случае нашего пула процессов, отсылают нас обратно к тому факту, что операции ввода/ вывода не значимы. Это позволяет нашему пулу процессов оказаться быстрее, так как в отличии от потоков они не требуют механизма синхронизации (как это пояснялось в рецепте Введение в параллельное программирование Python из Главе 1, Приступая к параллельному программированию и Python {Прим. пер.: так в авторском тексте. Более правильной причиной более плохого времени исполнения пула потоков по сравнению с пулом процессов, всё же кажется фактическое последовательное исполнение пула потоков.}.

Также ознакомьтесь...

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

Многие иные приложения, тем не менее, требуют чтобы всякое действие выполнялось немедленно или чтобы вы имели больше контроля над тем потоком, который его выполняет: в этом случае слияние в пул будет не лучшим выбором.

Дополнительно

Занимательное руководство по concurrent.futures можно найти по следующей ссылке.

Управление основным циклом событий через asyncio

Модуль Python asyncio предоставляет средства для управления событиями , сопрограммами, задачами, а также потоками и примитивами синхронизации для написания одновременного кода.

Основные компоненты этого модуля таковы:

Цикл событий: Модуль asyncio делает возможным один цикл событий на процесс. Именно это тот логический элемент, который имеет дело с управлением исполнения различных задач и их распределением. В частности, он регистрирует имеющиеся задачи и управляет ими переключая свой поток управления с одной задачи на другую.

Сопрограммы: Это обобщение имеющегося понятия подпрограммы. Кроме того, некая сопрограмма способна приостанавливаться в процессе исполнения для ожидания внешней обработки (неких процедур с вводом/ выводом) и возобновляться с точки останова после внешней обработки.

Фьючерсы: Определяют объект Future в точности как это делает модуль concurrent.futures. Они представляют ещё не выполненные вычисления.

Задачи: Это подкласс asyncio, который применяется для инкапсуляции сопрограмм и управления ими в параллельном режиме.

В этом рецепте мы сосредоточимся на концепции событий и управление событием (а именно, на цикле событий) внутри некой программы.

Понимание циклов событий

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

Внутри системы тот логический элемент, который способен вырабатывать события имеет название источника событий (event source), в то время как тот логический элемент, который имеет дело с обработкой какого- то сособного произойти события это обработчик события (event handler).

Программная конструкция Цикла событий (event loop) реализует необходимую функциональность управления событиями в рамках программы. Более точно, такой цикл событий работает циклически в процессе всего исполнения данной программы, отслеживая события, которые происходят внутри некой структуры данных постановки в очередь, а затем обрабатывают их по одному за раз вызывая надлежащий обработчик событий, когда его основной поток свободен.

Здесь отображён пример псевдокода такого цикла событий:

while (1) {
    events = getEvents()
    for (e in events)
        processEvent(e)
}

Все события, которые поданы в данный цикл while улавливаются и затем обрабатываются своим обработчиком событий. Соответствующий обработчик событий, который обрабатывает некое событие это всего лишь действие, имеющее место в данной системе. После завершения данного обработчика, управление передаётся определённому запланированному следующему событию.

Для управления неким циклом событий asyncio предоставляет следующие методы:

loop = get_event_loop(): Получает для текущего контекста надлежащий цикл событий.

loop.call_later(time_delay,callback,argument): Этим выставляется необходимый обратный вызов, который следует выполнить через заданный в секундах time_delay.

loop.call_soon(callback, argument): Выставляет обратный вызов, подлежащий настолько быстрому, насколько это возможно вызову . Такой вызываемый после call_soon() обратный вызов (подробнее...) возвращается после возврата управления в данный цикл событий.

loop.time(): Возвращает текущую величину времени в виде значения float (подробнее...), согласно внутренним часам данного цикла событий.

asyncio.set_event_loop(): Закольцовывает необходимый цикл событий для данного текущего контекста.

asyncio.new_event_loop(): Создаёт и возвращает некий новый объект цикла событий в соответствии с данными правилами политики.

loop.run_forever(): Выполняется вплоть до вызова stop() (подробнее...).

Как это сделать...

В данном примере мы рассмотрим как использовать предоставляемый библиотекой asyncio оператор цикла событий для построения неого работающего в асинхронном режиме приложения.

Для этого примера мы определяем три задачи. Каждая из задач имеет некое время исполнения, задаваемое случайным параметром времени. После завершения исполнения Task A вызывает Task B, Task B вызывает Task C, а Task C вызывает Task A.

Данный цикл событий будет продолжаться пока не осуществится некое условие прекращения. Как мы можем себе представить, этот пример следует такой асинхронной схеме:

Рисунок 5-1

Модель асинхронного программирования

Давайте рассмотрим следующие шаги:

Начнём с импорта тех библиотек, которые потребуются в нашей реализации:

import asyncio
import time
import random

Далее мы определяем task_A, чьё время исполнения определяется случайным образом и может меняться от 1 до 5 секунд. По завершению её исполнения, если не было выполнено условие прекращения, управление переходит к task_B:

def task_A(end_time, loop):
    print ("task_A called")
    time.sleep(random.randint(0, 5))
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, task_B, end_time, loop)
    else:
        loop.stop()

Здесь задаётся task_B. Её время исполнения определяется случайным образом и может меняться от 4 до 7 секунд. В самом конце выполнения при не удовлетворении условия прекращения вычисление переходит к task_C:

def task_B(end_time, loop):
    print ("task_B called ")
    time.sleep(random.randint(3, 7))
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, task_C, end_time, loop)
    else:
        loop.stop()

После этого реализуется task_C. Её время выполнения определяется случайным образом в диапазоне от 6 до 10 секунд. По окончанию её исполнения, когда не удовлетворяется условие прекращения, управление переходит к task_A:

def task_C(end_time, loop):
    print ("task_C called")
    time.sleep(random.randint(5, 10))
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, task_A, end_time, loop)
    else:
        loop.stop()

Наш следующий оператор определяет необходимый параметр loop, который просто получает значение текущего цикла событий:

loop = asyncio.get_event_loop()

Значение величины end_loop определяет необходимое условие прекращения. Выполнение данного примера должно продолжаться 60 секунд:

end_loop = loop.time() + 60

После этого давайте запросим выполнение task_A:

loop.call_soon(task_A, end_loop, loop)

Теперь мы настраиваем цикл длительного выполнения, который продолжит откликаться на события пока они не будут остановлены:

loop.run_forever()

После этого мы закрываем свой цикл событий:

loop.close()

Как это работает...

Для управления выполнением нашими тремя задачами, task_A, task_B и task_C, нам требуется перехватить установленный цикл событий:

loop = asyncio.get_event_loop()

Затем мы планируем самый первый вызов task_A при помощи конструкции call_soon:

end_loop = loop.time() + 60
loop.call_soon(function_1, end_loop, loop)

Давайте рассмотрим имеющееся определение task_A:

def task_A(end_time, loop):
    print ("task_A called")
    time.sleep(random.randint(0, 5))
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, task_B, end_time, loop)
    else:
        loop.stop()

Установленное асинхронное поведение нашего приложения определяется следующими параметрами:

time.sleep(random.randint(0, 5)): Определяет продолжительность времени выполнения данной задачи.

end_time: Задаёт верхний предел времени внутри task_A и выполняет вызов task_B через метод call_later.

loop: Это сам цикл событий, ранее перехваченный р помощи метода get_event_loop().

После выполнения этой задачи loop.time сравнивается с end_time. Если значение времени выполнения находится в рамках установленного максимального времени (60 секунд), тогда наше вычисление продолжается вызовом task_B, в противном случае наше выполнение завершается, закрывая и сам цикл событий:

if (loop.time() + 1.0) < end_time:
        loop.call_later(1, task_B, end_time, loop)
    else:
        loop.stop()

Для всех остальных задач все действия практически те же самые, за исключением значения времени выполнения и вызова следующей задачи.

Теперь позвольте мне подытожить положение дел:

task_A вызывает task_B со случайным значением времени исполнения между 1 и 5 секундами.

task_B вызывает task_C со случайным значением времени исполнения между 4 и 7 секундами.

task_C вызывает task_A со случайным значением времени исполнения между 6 и 10 секундами.

По истечениию отведённого на выполнение времени наш цикл должен завершиться:

loop.run_forever()
loop.close()

Возможный вывод данного примера выглядит следующим образом:

task_A called
task_B called
task_C called
task_A called
task_B called
task_C called
task_A called
task_B called
task_C called
task_A called
task_B called
task_C called
task_A called
task_B called
task_C called

Также ознакомьтесь...

Асинхронное программирование событий заменяет некий вид одновременного программирования при котором отдельные части определённой программы выполняются одновременно в различных потоках, которые имеют доступ к одним и тем же данным в памяти, тем самым порождая проблемы критичных запусков. В то же самое время возникла необходимость наличия возможности применения различных ядер современных ЦПУ, так как для определённых областей производительность, аналогичная предоставляемая процессорами уже не может достигаться в процессоре с единственным ядром.

Дополнительно

Вот неплохое введение в asyncio.

{Прим. пер.: обращаем также ваше внимание на наши переводы Полного руководство параллельного программирования на Python Куан Нгуена и Asyncio в Python 3 Цалеба Хаттингха.}

Обработка сопрограммам при помощи asyncio

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

В данном разделе мы введём некое обобщение имеющегося понятия подпрограмм, имеющее название сопрограмм (coroutines): также как и подпрограммы, сопрограммы выполняют некий отдельный вычислительный этап, однако в отличии от подпрограмм нет никакой программы main для координации получаемых результатов. Такие сопрограммы связывают себя самостоятельно в виде некого конвейера без какой бы то ни было функции наблюдения, которая бы отвечала за их вызов в каком- то определённом порядке.

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

такое чередование управляется устанавливаемым циклом событий, который был описан в рецепте Управление основным циклом событий через asyncio. Он отслеживает все имеющиеся сопрограммы и планирует их выполнение.

Прочие важные стороны сопрограмм таковы:

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

Сопрограммы могут передавать управление любой иной сопрограмме.

Сам термин выдача (yield) применяется здесь для описания приостановки сопрограммы и передаче потока управления в другую сопрограмму.

Приготовление

Для работы с сопрограммами мы будем пользоваться такой нотацией:

import asyncio

@asyncio.coroutine
def coroutine_function(function_arguments):
    ............
    DO_SOMETHING
    ............

Сопрограммы применяют синтаксис yield from, введённый PEP 380 для останова выполнения текущих вычислений и приостановки имеющегося внутреннего состояния сопрограммы.

На практике, в случае применения yield from future, данная сопрограмма приостанавливается до тех пор, пока делается future, затем будет распространён полученный результат future (либо возникнет исключительная ситуация); в случае же yield from coroutine, наша сопрограмма ожидает производства результата другой сопрограммой, который будет опубликован (либо возникнет исключительная ситуация).

Как мы увидим в своём следующем примере, в котором наши сопрограммы будут использованы для имитации некой машины конечных состояний, мы воспользуемся такой нотацией же yield from coroutine.

[Замечание]
Замечание

Дополнительные сведения о сопрограммах в asyncio доступны по ссылке.

{Прим. пер.: обращаем также ваше внимание на наши переводы Полного руководство параллельного программирования на Python Куан Нгуена и Asyncio в Python 3 Цалеба Хаттингха.}

Как это сделать...

В данном примере мы рассмотрим как применять сопрограммы для имитации машины конечных состояний из пяти состояний.

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

Тот автомат, поведение которого мы намерены имитировать пр помощи сопрограмм выглядит так:

Рисунок 5-2

Машина конечных состояний

Состояниями нашей системы являются S0, S1, S2, S3 и S4 со значениями 0 и 1: теми значениями, при которых наш автомат способен переходить из одного состояния в своё следующее состояние (эта операция имеет название перехода - transition). итак, например, состояние S0 способно перейти в состояние S1, но только для своего значения 1, и состояние S0 может перейти в состояние S2 только для своего значения 0.

аш следующий код имитирует некий переход нашего автомата из состояния S0 (начальное состояние) вплоть до состояния S4 (конечное, терминальное состояние):

Самый первый шаг, очевидно, импорт всех относящихся к делу библиотек:

import asyncio
import time
from random import randint

Далее мы определяем свою сопрограмму, относящуюся к start_state. Её параметр input_value вычисляется случайным образом; он может принимать значения 0 или 1. Когда он равен 0, тогда управление переходит сопрограмме state2; в противном случае она меняется на state1:

@asyncio.coroutine
def start_state():
    print('Start State called\n')
    input_value = randint(0, 1)
    time.sleep(1)
    if input_value == 0:
        result = yield from state2(input_value)
    else:
        result = yield from state1(input_value)
    print('Resume of the Transition:\nStart State calling'+ result)

Вот сопрограмма для state1 имеет параметр input_value, который позволяет передачу значения состояния. Как и в случае с input_value, он вычисляется случайным образом. Когда он равен 0, тогда управление переходит сопрограмме state2; в противном случае она меняется на state3:

@asyncio.coroutine
def state1(transition_value):
    output_value ='State 1 with transition value = %s\n'% \
                                             transition_value
    input_value = randint(0, 1)
    time.sleep(1)
    print('...evaluating...')
    if input_value == 0:
        result = yield from state3(input_value)
    else:
        result = yield from state2(input_value)
    return output_value + 'State 1 calling %s' % result

Аналогично и для сопрограммы state2 с параметром input_value. Когда он равен 0, тогда управление переходит сопрограмме state3; в противном случае она меняется на state1:

@asyncio.coroutine
def state2(transition_value):
    output_value = 'State 2 with transition value = %s\n' %\
                                             transition_value
    input_value = randint(0, 1)
    time.sleep(1)
    print('...evaluating...')
    if input_value == 0:
        result = yield from state1(input_value)
    else:
        result = yield from state3(input_value)
    return output_value + 'State 2 calling %s' % result

И, наконец у сопрограммы state2 параметр input_value также позволяет передавать управление. Когда он равен 0, тогда управление переходит сопрограмме state1; в противном случае управление отдаётся в end_state:

@asyncio.coroutine
def state3(transition_value):
    output_value = 'State 3 with transition value = %s\n' %\
                                                 transition_value
    input_value = randint(0, 1)
    time.sleep(1)
    print('...evaluating...')
    if input_value == 0:
        result = yield from state1(input_value)
    else:
        result = yield from end_state(input_value)
    return output_value + 'State 3 calling %s' % result

end_state выводит на печать значение параметра transition_value, который сделал возможным переход в это состояние и затем останавливает вычисления:

@asyncio.coroutine
def end_state(transition_value):
    output_value = 'End State with transition value = %s\n'%\
                                                transition_value
    print('...stop computation...')
    return output_value

В своей функции __main__ получается необходимый цикл событий, а затем мы начинаем свою имитацию машины конечных состояний, вызывая start_state своего автомата:

if __name__ == '__main__':
    print('Finite State Machine simulation with Asyncio Coroutine')
    loop = asyncio.get_event_loop()
    loop.run_until_complete(start_state())

Как это работает...

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

@asyncio.coroutine

Например, так определяется состояние S0:

@asyncio.coroutine
def StartState():
    print ("Start State called \n")
    input_value = randint(0,1)
    time.sleep(1)
    if (input_value == 0):
        result = yield from State2(input_value)
    else :
        result = yield from State1(input_value)

переход на следующее состояние задаётся input_value, которое определяется функцией randint (0,1) модуля Python random. Эта функция случайным образом производит некое значение 0 или 1.

Тем самым, randint случайно определяет то состояние, в которое перейдёт наша машина конечных состояний:

input_value = randint(0,1)

После определения передаваемых значений, текущая сопрограмма вызывает свою следующую сопрограмму при помощи команды yield from:

if (input_value == 0):
        result = yield from State2(input_value)
    else :
        result = yield from State1(input_value)

Значением переменной result является то значение, которое возвращается каждой сопрограммой. Это некая строка и, по завершению вычислений, мы имеем возможность реконструкции всего перехода от начального состояния своего автомата, start_state, вплоть до end_state.

Наша программа main запускает вычисления внутри установленного цикла событий:

if __name__ == "__main__":
    print("Finite State Machine simulation with Asyncio Coroutine")
    loop = asyncio.get_event_loop()
    loop.run_until_complete(StartState())

Исполнив полученный код мы получаем вывод, подобный следующему:

Finite State Machine simulation with Asyncio Coroutine
Start State called
...evaluating...
...evaluating...
...evaluating...
...evaluating...
...stop computation...
Resume of the Transition :
Start State calling State 1 with transition value = 1
State 1 calling State 2 with transition value = 1
State 2 calling State 1 with transition value = 0
State 1 calling State 3 with transition value = 0
State 3 calling End State with transition value = 1

Также ознакомьтесь...

Вплоть до выхода Python 3.5, его модуль asyncio применял генераторы для подражания асинхронным вызовам, а следовательно, имел иной синтаксис чем у текущей версии Python 3.5.

Python 3.5 ввёл ключевые слова async и await. Обратите внимание на отсутствие скобок вокруг вызова await func().

Ниже приводится некий образец "Hello, world!", применяющий такой новый синтаксис asyncio, введённый Python 3.5+:

import asyncio

async def main():
    print(await func())

async def func():
    # Do time intensive stuff...
    return "Hello, world!"

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Дополнительно

Хорошее описание сопрограмм в Python имеется в следующем блоге.

{Прим. пер.: обращаем также ваше внимание на наши переводы Полного руководство параллельного программирования на Python Куан Нгуена и Asyncio в Python 3 Цалеба Хаттингха.}

Манипулирование задачами при помощи asyncio

Модуль asyncio спроектирован для обработки асинхронных процессов и исполнения одновременных задач поверх некого цикла событий. Он также предоставляет свой класс asyncio.Task() для целей обёртывания сопрограмм в некой задаче (подробнее...). Его использование делает возможным независимо запускаемым задачам исполняться одновременно с прочими задачами в одном и том же цикле событий.

Когда некая сопрограмма обёртывается в какую- то задачу, он подключает Task к имеющемуся циклу задач и затем запускается автоматически после старта самого цикла, тем самым предоставляя некий механизм автоматического продвижения такой сопрограммы.

Модуль asyncio предоставляет метод asyncio.Task(coroutine) для обработки вычислений посредством задач; более того, asyncio.Task(coroutine) составляет расписание соответствующего исполнения сопрограммы (подробнее...)

.

Задача отвечает за выполнение объекта сопрограммы в каком- то цикле событий.

Когда такая обёрнутая сопрограмма применяет установленную нотацию yields from future, как это уже описывалось в нашем разделе Обработка сопрограммам при помощи asyncio, тогда такая задача приостанавливает своё выполнение обёрнутой сопрограммы и дожидается (awaits) завершение соответствующего фьючерса (future).

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

Пока некая задача дожидается завершения какого- то фьючерса, её цикл событий исполняет новую задачу.

Как это сделать...

В данном примере мы покажем как наш оператор asyncio.Task() способен выполнять одновременно три математические функции:

Естественно, давайте начнём с импорта необходимой библиотеки asyncio:

import asyncio

В самой первой сопрограмме определяется функция factorial:

@asyncio.coroutine
def factorial(number):
    f = 1
    for i in range(2, number + 1):
        print("Asyncio.Task: Compute factorial(%s)" % (i))
        yield from asyncio.sleep(1)
        f *= i
    print("Asyncio.Task - factorial(%s) = %s" % (number, f))

После которой определяется наша вторая функция - функция fibonacci:

@asyncio.coroutine
def fibonacci(number):
    a, b = 0, 1
    for i in range(number):
        print("Asyncio.Task: Compute fibonacci (%s)" % (i))
        yield from asyncio.sleep(1)
        a, b = b, a + b
    print("Asyncio.Task - fibonacci(%s) = %s" % (number, a))

И самая последняя подлежащая одновременному исполнению функция это вычисление биноминального коэффициента:

@asyncio.coroutine
def binomial_coefficient(n, k):
    result = 1
    for i in range(1, k + 1):
        result = result * (n - i + 1) / i
        print("Asyncio.Task: Compute binomial_coefficient (%s)" %
            (i))
        yield from asyncio.sleep(1)
    print("Asyncio.Task - binomial_coefficient(%s , %s) = %s" %
        (n,k,result))

В нашей программе __main__ task_list содержит все функции, которые должны выполняться параллельно при помощи функции asyncio.Task:

if __name__ == '__main__':
    task_list = [asyncio.Task(factorial(10)),
                 asyncio.Task(fibonacci(10)),
                 asyncio.Task(binomial_coefficient(20, 10))]

Наконец, мы обзаводимся необходимым циклом событий и запускаем свои вычисления:

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task_list))
loop.close()

Как это работает...

Все сопрограммы определяются с помощью необходимой аннотации @asyncio.coroutine (имеющей название декоратора):

@asyncio.coroutine
def function (args):
    do something

Для параллельного запуска каждая функция выступает неким аргументом в соответствующем модуле asyncio.Task и таким образом они включаются в task_list:

if __name__ == '__main__':
    task_list = [asyncio.Task(factorial(10)),
                 asyncio.Task(fibonacci(10)),
                 asyncio.Task(binomial_coefficient(20, 10))]

Затем мы получаем необходимый цикл событий:

loop = asyncio.get_event_loop()

Наконец, мы добавляем это выполнение task_list в полученный цикл событий:

loop.run_until_complete(asyncio.wait(task_list))
loop.close()

[Совет]
Совет

Обратите внимание, что наш оператор asyncio.wait(task_list) дожидается завершения приданных ему сопрограмм.

Вывод нашего предыдущего кода выглядит как- то так:

Asyncio.Task: Compute factorial(2)
Asyncio.Task: Compute fibonacci(0)
Asyncio.Task: Compute binomial_coefficient(1)
Asyncio.Task: Compute factorial(3)
Asyncio.Task: Compute fibonacci(1)
Asyncio.Task: Compute binomial_coefficient(2)
Asyncio.Task: Compute factorial(4)
Asyncio.Task: Compute fibonacci(2)
Asyncio.Task: Compute binomial_coefficient(3)
Asyncio.Task: Compute factorial(5)
Asyncio.Task: Compute fibonacci(3)
Asyncio.Task: Compute binomial_coefficient(4)
Asyncio.Task: Compute factorial(6)
Asyncio.Task: Compute fibonacci(4)
Asyncio.Task: Compute binomial_coefficient(5)
Asyncio.Task: Compute factorial(7)
Asyncio.Task: Compute fibonacci(5)
Asyncio.Task: Compute binomial_coefficient(6)
Asyncio.Task: Compute factorial(8)
Asyncio.Task: Compute fibonacci(6)
Asyncio.Task: Compute binomial_coefficient(7)
Asyncio.Task: Compute factorial(9)
Asyncio.Task: Compute fibonacci(7)
Asyncio.Task: Compute binomial_coefficient(8)
Asyncio.Task: Compute factorial(10)
Asyncio.Task: Compute fibonacci(8)
Asyncio.Task: Compute binomial_coefficient(9)
Asyncio.Task - factorial(10) = 3628800
Asyncio.Task: Compute fibonacci(9)
Asyncio.Task: Compute binomial_coefficient(10)
Asyncio.Task - fibonacci(10) = 55
Asyncio.Task - binomial_coefficient(20, 10) = 184756.0

Также ознакомьтесь...

asyncio предоставляет и иные способы планирования задач при помощи методов ensure_future() или AbstractEventLoop.create_task(), причём оба они получают некий объект сопрограммы.

Дополнительно

Дополнительные сведения относительно задач asyncio можно почерпнуть в следующем руководстве.

{Прим. пер.: обращаем также ваше внимание на наши переводы Полного руководство параллельного программирования на Python Куан Нгуена и Asyncio в Python 3 Цалеба Хаттингха.}

Работа с asyncio и фьючерсы

Другим ключевым компонентом модуля asyncio выступает его класс asyncio.Future. Он очень похож на concurrent.Futures, но, конечно же, он адаптирован под самый основной механизм asyncio: его цикл событий.

Наш класс asyncio.Future представляет некий результат (но это также может быть некая исключительная ситуация), который пока ещё не доступен.

Тем самым он представляет некую абстракцию чего- то что ещё не достигнуто. Те обратные вызовы, которые и должны вырабатывать все результаты, по существу, добавляются в экземпляры этого класса.

Приготовление

Для определения некого объекта future следует применять такой синтаксис:

future = asyncio.Future

Основные методу управления данным объектом такие:

cancel(): Отменяет данный объект future и планирует обратные вызовы.

result(): Возвращает полученные результаты, представленные данным future.

exception(): Возвращает значение исключительной ситуации, установленной в данном future.

add_done_callback(fn): Добавляет некий обратный вызов, подлежащий исполнению после выполнения future.

remove_done_callback(fn): Удаляет все экземпляры некого обратного вызова из данного вызова по завершению.

set_result(result): Помечает future как выполненный и устанавливает его результат.

set_exception(exception): Помечает future как выполненный и устанавливает его исключительную ситуацию.

Как это сделать...

Наш следующий пример показывает как применять класс asyncio.Future для управления двумя сопрограммами: first_coroutine и second_coroutine, которые выполняют такие задачи. first_coroutine выполняет суммирование первых N целых, а second_coroutine вычисляет значение факториала N:

Сейчас давайте импортируем относящиеся к делу библиотеки:

import asyncio
import sys

first_coroutine реализует необходимую функцию sum для первых N целых:

@asyncio.coroutine
def first_coroutine(future, num):
    count = 0
    for i in range(1, num + 1):
        count += i
    yield from asyncio.sleep(1)
    future.set_result('First coroutine (sum of N integers)\
                      result = %s' % count)

В second_coroutine мы всё ещё реализуем соответствующую функцию factorial:

@asyncio.coroutine
def second_coroutine(future, num):
    count = 1
    for i in range(2, num + 1):
        count *= i
    yield from asyncio.sleep(2)
    future.set_result('Second coroutine (factorial) result = %s' %\
                      count)

Воспользовавшись функцией got_result мы выводим на печать полученное в результате вычислений:

def got_result(future):
    print(future.result())

В нашей функции main параметры num1 должны быть установлены пользователем. Они будут применяться в качестве параметров для тех функций, которые реализованы в первой и второй сопрограммах:num2

if __name__ == "__main__":
    num1 = int(sys.argv[1])
    num2 = int(sys.argv[2])

Теперь давайте получим необходимый цикл событий:

loop = asyncio.get_event_loop()

Здесь посредством функции asyncio.future определяются наши фьючерсы:

future1 = asyncio.Future()
future2 = asyncio.Future()

Наши две сопрограммы - first_couroutine и second_couroutine - включённые в установленный список tasks имеют фьючерсы future1 и future2, определённые пользователем аргумента и значения параметров num1 и num2:

tasks = [first_coroutine(future1, num1),
        second_coroutine(future2, num2)]

Данные фьючерсы добавляются как некий обратный вызов:

future1.add_done_callback(got_result)
future2.add_done_callback(got_result)

Затем установленный список tasks добавляется к имеющемуся циклу событий чтобы можно было приступить к вычислениям:

loop.run_until_complete(asyncio.wait(tasks))
loop.close()

Как это работает...

В своей программе main мы определяем объекты future, соответственно future1 и future2, воспользовавшись директивой asyncio.Future():

if __name__ == "__main__":
        future1 = asyncio.Future()
        future2 = asyncio.Future()

При определении своих задач, мы передаём необходимые объекты future как аргументы своих двух сопрограмм first_couroutine и second_couroutine:

tasks = [first_coroutine(future1,num1),
         second_coroutine(future2,num2)]

Наконец, мы добавляем некий обратный вызов, который надлежит исполнить после выполнения future:

future1.add_done_callback(got_result)
future2.add_done_callback(got_result)

Здесь got_result это некая функция, которая выводит на печать полученные future результаты:

def got_result(future):
    print(future.result())

В саму сопрограмму мы передаём необходимый объект future в качестве какого- то аргумента. После его вычисления мы устанавливаем время sleep в 3 секунды для своей первой сопрограммы и в 4 секунды для второй:

yield from asyncio.sleep(sleep_time)

Приводимый ниже вывод получен путём вычисления данной команды с различными значениями:

> python asyncio_and_futures.py 1 1
First coroutine (sum of N integers) result = 1
Second coroutine (factorial) result = 1

> python asyncio_and_futures.py 2 2
First coroutine (sum of N integers) result = 2
Second coroutine (factorial) result = 2

> python asyncio_and_futures.py 3 3
First coroutine (sum of N integers) result = 6
Second coroutine (factorial) result = 6

> python asyncio_and_futures.py 5 5
First coroutine (sum of N integers) result = 15
Second coroutine (factorial) result = 120

> python asyncio_and_futures.py 50 50
First coroutine (sum of N integers) result = 1275
Second coroutine (factorial) result = 30414093201713378043612608166064768844377641568960512000000000000
First coroutine (sum of N integers) result = 1275

Также ознакомьтесь...

Мы можем поменять местами получаемые результаты, то есть первым получить вывод second_coroutine, простой перестановкой времён сна между своими сопрограммами: yield from asyncio.sleep(2) в определении first_coroutine и yield from asyncio.sleep(1) в определении second_coroutine. Это можно показать в таком примере:

> python asyncio_and_future.py 1 10
second coroutine (factorial) result = 3628800
first coroutine (sum of N integers) result = 1

Дополнительно

Дополнительные примеры asyncio и фьючерсов можно найти в следующем блоге.

Глава 8. Неоднородные вычисления

Содержание

Глава 8. Неоднородные вычисления

Основы неоднородных вычислений

Основы архитектуры GPU

Основы программирования GPU

CUDA

OpenCL

Работа с PyCUDA

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Гетерогенное программирование с применением PyCUDA

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Реализация управления памятью с применением PyCUDA

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Введение в PyOpenCL

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Построение приложений при помощи PyOpenCL

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Поэлементные выражения с применением PyOpenCL

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Оценка приложений PyOpenCL

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

За OpenCL и PyOpenCL

Против OpenCL и PyOpenCL

За CUDA и PyCUDA

Против CUDA и PyCUDA

Дополнительно

Программирование GPU с применением Numba

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Данная глава поможет нам изучить технологии программирование GPU (Graphics Processing Unit) с применением языка Python. Продолжающееся развитие GPU выявляет насколько великие возможности эти архитектуры способны привносить в осуществление сложных вычислений.

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

Мы изучим основные среды разработки для неоднородного (гетерогенного) программирования, а именно, среды PyCUDA и Numba для CUDA (Compute Unified Device Architecture), а также PyOpenCL, который предназначен для инфраструктуры OpenCL (Open Computing Language) в своей версии Python.

{Прим. пер.: во второй половине ноября 2019 года мы намерены выложить в Дополнении к этому переводу описание интерфейса Python для нового процессора Fujitsu (DLU), выпускаемого ими в сотрудничестве с Intel 5 ноября 2019.}

В этой главе мы охватим такие рецепты:

Основы неоднородных вычислений

Понимание архитектуры GPU

Основы программирования GPU

Работа с PyCUDA

Неоднородное программирование в PyCUDA

Реализация управления памятью при помощи PyCUDA

Введение в PyOpenCL

Построение приложения с применением PyOpenCL

Поэлементные выражения PyOpenCL

Вычисление приложений PyOpenCL

GPU программирование с применением Numba

Давайте начнём с более подробного понгимания неоднородных вычислений.

Основы неоднородных вычислений

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

В этом контексте, тот процессор, в котором запущена основная программа (как правило, ЦПУ) именуется хостом, в то время как имеющиеся сопроцессоры (например, GPU) имеют название устройств. Эти последние обычно физически обособлены в своём хосте и управляют своим собственным пространством памяти, которое также обособлено от памяти хоста.

В частности, следуя за значительным спросом на рынке, такие GPU превратились в процессор с высокой параллельностью, преобразовав графический процессор из устройства построения графики в устройства для параллельных и интенсивных вычислений общего назначения.

В действительности применение GPU для задач, отличных от построения графического изображения на экране и имеет название неоднородных вычислений.

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

Основы архитектуры GPU

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

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

Некий GPU (см. следующую схему) состоит из определённого числа вычислительных устройств, носящих название Потоковых мультипроцессоров (SM, Streaming Multiprocessors), которые являют собой самый первый уровень параллелизма. На практике каждый SM работает одновременно и независимо от прочих.

Рисунок 8-1

Архитектура GPU

Каждый SM делится на некую группу Потоковых процессоров (SP, Streaming Processors), обладающих неким ядром, способным последовательно выполнять некий поток. Такой SP представляет наименьший элемент логики выполнения и самый тонкий уровень параллельности.

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

Основы программирования GPU

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

В наши дни GPU способен выполнять классические инструкции ЦПУ, такие как циклы и условия , доступ к памяти и вычисления с плавающей точкой. Два основных производителя дискретных видео карт - NVIDIA и AMD - разработали собственные архитектуры GPU, снабжая разработчиков соответствующими средами разработки, которые позволяют программировать на различных языках программирования, включая Python. {Прим. пер.: во второй половине ноября 2019 года мы намерены выложить в Дополнении к этому переводу описание интерфейса Python для нового процессора Fujitsu (DLU), выпускаемого ими в сотрудничестве с Intel 5 ноября 2019.}

В настоящее время разработчики обладают ценными инструментами для написания программного обеспечения, которое применяет GPU в не связанном с графикой контексте. Среди основных сред разработки для неоднородных вычислений у нас имеются CUDA и OpenCL.

Давайте рассмотрим их подробнее.

CUDA

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

CUDA предлагает расширения для наиболее применяемых языков применяемых в программировании, в том числе и Python. Наиболее хорошо известными расширениями CUDA Python являются:

PyCUDA

Numba

В последующих разделах мы рассмотрим их подробнее.

OpenCL

Вторым главным действующим лицом в параллельных вычислениях выступает OpenCL, который (в отличие от своего аналога nVIDIA) является открытым стандартом и может применяться не только с графическими процессорами разных производителей, но и с микропроцессорами разных типов.

Тем не менее, OpenCL является более полным и универсальным решением, пока не обладающим такой зрелости и простоты применения как CUDA.

Расширением OpenCL в Python выступает PyOpenCL.

В наших последующих разделах будут проанализированы модели программирования CUDA и OpenCL в их расширениях Python и они будут сопровождены некоторыми занятными примерами использования.

Работа с PyCUDA

PyCUDA это библиотека привязок, обеспечивающая доступ к API Python CUDA от Андреаса Клёкнера. Основные функции содержат автоматическую очистку, которая привязана к времени жизни объектов, что предотвращает утечки памяти, удобную абстракцию поверх модулей и буферов, полный доступ к драйверу и встроенную обработку ошибок.

Данный продукт является открытым исходным кодом с лицензией MIT, его документация очень ясная, к тому же множество находящихся в открытом доступе исходных кодов могут оказать содействие и поддержку. Основная цель PyCUDA состоит в том, чтобы позволить разработчику вызывать CUDA с минимумом абстракций Python и также поддерживать метапрограммирование и шаблоны CUDA.

Приготовление

Для установки PyCUDA, будьте любезны следовать инструкциям домашней страницы Андреаса Клёкнера.

Наш следующий пример программирования имеет двойную функцию:

Прежде всего убедиться что PyCUDA установлен как подобает.

Во- вторых, считать и вывести на печать характеристики имеющихся карт GPU.

Как это сделать...

Давайте рассмотрим следующие шаги:

Самой первой инструкцией мы импортируем имеющийся драйвер Python (то есть, pycuda.driver) в установленной на вашем ПК библиотеке CUDA:

import pycuda.driver as drv

Инициализируем CUDA. Обратите внимание, что следующую инструкцию следует вызывать прежде всех прочих инструкций из модуля pycuda.driver:

drv.init()

Пересчитываем общее число карт GPU в вашем ПК:

print ("%d device(s) found." % drv.Device.count())

Для каждой из установленных GPU карт выводим на печать название их модели, вычислительные возможности, а также общий объём памяти данного устройства в килоБайтах:

for ordinal i n range(drv.Device.count()):
       dev = drv.Device(ordinal)
       print ("Device #%d: %s" % (ordinal, dev.name())
       print ("Compute Capability: %d.%d"% dev.compute_capability())
       print ("Total Memory: %s KB" % (dev.total_memory()//(1024)))

Как это работает...

Всё исполнение достаточно незатейливое. В самой первой строке кода импортируется pycuda.driver, а затем и инициализируется:

import pycuda.driver as drv
drv.init()

Этот модуль pycuda.driver выставляет уровень драйвера для интерфейса программирования CUDA, что более удобно нежели интерфейс программирования времени исполнения C CUDA, к тому же он обладает рядом функциональных возможностей, которые не представлены в интерфейсе времени выполнения.

Затем он обходит в цикле значение функции drv.Device.count(), причём для каждой карты GPU выводятся на печать название этой карты и её основные характеристики (вычислительные возможности и общий объём памяти):

print ("Device #%d: %s" % (ordinal, dev.name()))
print ("Compute Capability: %d.%d" % dev.compute_capability())
print ("Total Memory: %s KB" % (dev.total_memory()//(1024)))

Выполняем следующий код:

C:\> python dealingWithPycuda.py

После его выполнения на экране будут отображены все установленные GPU, примерно как в следующем образце:

1 device(s) found.
Device #0: GeForce GT 240
Compute Capability: 1.2
Total Memory: 1048576 KB

Также ознакомьтесь...

Модель программирования CUDA (а следовательно и PyCUDA, которая является обёрткой Python) реализуется через особые расширения стандартной библиотеки, написанной на языке программирования С. Эти расширения были созданы просто как вызовы функции из стандартной библиотеки C, делая возможным простой подход к модели неоднородного программирования, которая содержит код и для своего хоста, и для устройства. Собственно управление такими двумя логическими частями выполняется компилятором nvcc.

Вот краткое описание того как это работает:

Выделяем код устройства из кода хоста.

Вызываем некий компилятор по умолчанию (к примеру, GCC), для компиляции кода хоста.

Строим необходимый код устройства в двоичном виде (объекты .cubin) или в ассемблерном виде (объекты PTX):

Рисунок 8-2

Модель работы PyCUDA

Все предыдущие шаги выполняются со стороны PyCUDA в процессе выполнения с неким ростом в нагрузке времени исполнения по сравнению с приложением CUDA.

Дополнительно

Руководство по программированию CUDA доступно тут

Документация PyCUDA доступна здесь

Гетерогенное программирование с применением PyCUDA

Модель программирования CUDA (а следовательно и PyCUDA) разработана для того чтобы соединить программное приложение ЦПУ и GPU для выполнения имеющейся последовательной части в установленном ЦПУ, а той части, которая может выполняться параллельно, в присутствующем GPU. К сожалению, наш компьютер не достаточно сообразителен чтобы разбираться в том как автономно распределять полученный код, а потому именно разработчик сверху должен указывать какие части следует выполнять в ЦПУ, а какие в GPU.

На практике приложение CUDA составляется из неких последовательных компонентов, которые выполняются ЦПУ системы или хоста, либо параллельных компонентов, носящих названия ядер (kernels), которые исполняются вместо этого GPU или устройством.

Некое ядро определяется как решётка (сетка, grid) и в свою очередь может подвергаться декомпозиции на блоки, которые последовательно назначаются различным мультипроцессорам, тем самым реализуя параллелизм крупной грануляции (coarse-grained parallelism). Внутри этих блоков присутствуют основополагающие вычислительные элементы, потоки, с очень тонкой степенью параллельной грануляции (fine parallel granularity). некий поток может относиться только к одному блоку и идентифицироваться неким уникальным индексом для блоков и трёхмерными индексами для потоков. В них имеющиеся ядра выполняются последовательно. Блоки же и потоки, в свою очередь, выполняются параллельно. Общее число запущенных (параллельно) потоков зависит от их организации в блоки, а также от их запросов в плане ресурсов в соответствии с теми ресурсами, которые доступны в данном устройстве.

[Замечание]
Замечание

Для визуализации приведённых выше понятий, не сочтите за труд обратиться к Figure 5.

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

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

Выделяем память в своём хосте.

Пересылаем данные из имеющейся памяти хоста в память необходимого устройства.

Запускаем своё устройство через вызов имеющихся функций ядра.

Пересылаем результаты из памяти устройства в память его хоста.

Высвобождаем память, выделенную данному устройству.

Следующая схема отображает обсуждаемый поток выполнения программы в соответствии с моделью программирования PyCUDA:

Рисунок 8-3

Модель программирования PyCUDA

В своём следующем примере мы пройдёмся по конкретному примеру данной методологии программирования чтобы следовать порядку построения приложений PyCUDA.

Как это сделать...

Для демонстрации модели программирования PyCUDA мы рассмотрим задачу дублирования всех элементов в матрице 5 х 5:

Мы импортируем те библиотеки, которые необходимы для задач, которые мы собрались решать:

import PyCUDA.driver as CUDA
import PyCUDA.autoinit
from PyCUDA.compiler import SourceModule
import numpy

И импортируемая нами библиотека numpy позволяет нам строить ввод для нашей задачи, а именно, матрицу 5 x 5, значения которой выбираются случайным образом:

a = numpy.random.randn(5,5)
a = a.astype(numpy.float32)

Наша построенная подобным образом матрица должна быть скопирована из основной памяти своего хоста в память выполняющего вычисления устройства. Для этого мы выделяем некое пространство памяти (a_gpu) в этом устройстве, которое требуется для хранения матрицы a. С этой целью мы применяем функцию mem_alloc, которая должна в качестве своего предмета должна выделить необходимое пространство памяти. В частности, общее число байт матрицы a выражается значением параметра a.nbytes следующим образом:

a_gpu = cuda.mem_alloc(a.nbytes)

После этого мы сможем переслать свою матрицу из главного хоста в ту область памяти, которая создана целенаправленно в вычислительном устройстве при помощи функции memcpy_htod:

cuda.memcpy_htod(a_gpu, a)

Внутри вычислительного устройства будет работать функция ядра doubleMatrix. Её целью будет умножение всех элементов матрицы на 2. Как вы можете видеть, синтаксис этой функции doubleMatrix аналогичен С, в то время как наш оператор SourceModule является реальной директивой для компилятора nVidia (компилятор nvcc), создающий некий модуль, который в данном случае состоит всего лишь из нашей функции doubleMatrix:

mod = SourceModule("""
  __global__ void doubles_matrix(float *a){
    int idx = threadIdx.x + threadIdx.y*4;
    a[idx] *= 2;}
  """)

При помощи параметра func мы указываем свою функцию doubleMatrix, которая содержится в нашем модуле mod:

func = mod.get_function("doubles_matrix")

Наконец, мы запускаем свою функцию ядра. Для успешного выполнения некой функции ядра в имеющемся устройстве, пользователь CUDA должен задать необходимый ввод для этого ядра и значение размера своего блока потока. В нашем следующем случае имеется входная матрица a_gpu, которая ранее была скопирована в это устройство, в то время как размерность нашего блока потока равна (5,5,1):

func(a_gpu, block=(5,5,1))

Следовательно, мы выделяем область памяти по размеру эквивалентную своей входной матрице a:

a_doubled = numpy.empty_like(a)

Далее мы копируем содержимое области памяти выделенное устройству - то есть свою матрицу a_gpu - в ранее определённую область памяти, a_doubled:

cuda.memcpy_dtoh(a_doubled, a_gpu)

Наконец, мы выводим на печать содержимое своей входной матрицы и полученную в результате матрицу чтобы убедиться в качестве своей реализации:

print ("ORIGINAL MATRIX")
print (a)
print ("DOUBLED MATRIX AFTER PyCUDA EXECUTION")
print (a_doubled)

Как это работает...

Давайте начнём с просмотра импортируемых нами библиотек:

import PyCUDA.driver as CUDA
import PyCUDA.autoinit
from PyCUDA.compiler import SourceModule

В частности, наш импорт autoinit автоматически определяет какой именно GPU в нашей системе доступен для работы, в то время как SourceModule является директивой для того компилятора nVidia (nvcc), который позволит нам указать неободимые объекты, подлежащие компиляции и выгрузке в соответствующее устройство.

Далее при помощи библиотеки numpy мы строим свою входную матрицу 5 х 5:

import numpy
a = numpy.random.randn(5,5)

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

a = a.astype(numpy.float32)

Далее мы копируем свой массив из основного хоста в имеющееся устройство при помощи следующих двух операций:

a_gpu = CUDA.mem_alloc(a.nbytes)
CUDA.memcpy_htod(a_gpu, a)

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

Также следует обратить внимание что наша матрица a_gpu линеаризована, то есть имеет одно измерение, а следовательно должна обрабатываться именно так.

Более того, все эти действия не требуют вызовов ядра. Это означает, что они напрямую выполняются самим хостом.

Логический элемент SourceModule делает возможным определение нашей функции ядра doubleMatrix. __global__, который выступает в роли директивы nvcc, указывает на то, что данная функция doubleMatrix будет обрабатываться в вычислительном устройстве:

mod = SourceModule("""
  __global__ void doubleMatrix(float *a)

Давайте рассмотрим тело нашего ядра. Значение параметра idx является индексом матрицы, которая посредством threadIdx.x и threadIdx.y указывает координаты потока:

int idx = threadIdx.x + threadIdx.y*4;
a[idx] *= 2;

Далее mod.get_function("doubleMatrix") возвращает некий идентификатор для значения параметра func:

func = mod.get_function("doubleMatrix ")

Для выполнения своего ядра нам требуется настроить значение контекста исполнения. Это означает настройку соответствующей трёхмерной структуры имеющихся потоков, которые относятся к создаваемому блоку решётки при помощи значения параметра блока внутри самого вызова func:

func(a_gpu, block = (5, 5, 1))

block = (5, 5, 1) сообщает нам что мы вызываем некую функцию ядра с соответствующей лианеризованной входной матрицей a_gpu и единственным блоком с размером 5 (то есть с 5 потоками) в направлении x-, 5 потоками по направлению y- и 1 потоком в направлении z-, что создаёт в сумме 16 потоков. Обратите внимание, что каждый из потоков исполняет один и тот же код ядра (в сумме 25 потоков).

После вычисления в имеющемся устройстве GPU мы пользуемся неким массивом для сохранения полученных результатов:

a_doubled = numpy.empty_like(a)
CUDA.memcpy_dtoh(a_doubled, a_gpu)

Для выполнения нашего примера в приглашении своей Командной строки наберите следующее:

C:\> python heterogenousPycuda.py

Получаемый вывод должен выглядеть как- то так:

ORIGINAL MATRIX
[[-0.59975582 1.93627465 0.65337795 0.13205571 -0.46468592]
[ 0.01441949 1.40946579 0.5343408 -0.46614054 -0.31727529]
[-0.06868593 1.21149373 -0.6035406 -1.29117763 0.47762445]
[ 0.36176383 -1.443097 1.21592784 -1.04906416 -1.18935871]
[-0.06960868 -1.44647694 -1.22041082 1.17092752 0.3686313 ]]

DOUBLED MATRIX AFTER PyCUDA EXECUTION
[[-1.19951165 3.8725493 1.3067559 0.26411143 -0.92937183]
[ 0.02883899 2.81893158 1.0686816 -0.93228108 -0.63455057]
[-0.13737187 2.42298746 -1.2070812 -2.58235526 0.95524889]
[ 0.72352767 -2.886194 2.43185568 -2.09812832 -2.37871742]
[-0.13921736 -2.89295388 -2.44082164 2.34185504 0.73726263 ]]

Также ознакомьтесь...

Основным ключевым свойством CUDA, которое делает эту модель программирования по большей части отличающейся от прочих моделей программирования (обычно применяемых в ЦПУ), состоит в том, что для достижения эффективности она требует задействовать тысячи потоков. Это делает возможным в обычных структурах GPU, которые применяют потоки с малым весом, а также допускают создание и изменение контекста выполнения очень быстрым и действенным образом.

Обратите внимание, что планирование потоков напрямую связано с имеющейся архитектурой GPU и присущему ей параллелизму. На практике некий блок потоков назначается отдельному SM. Здесь все потоки далее делятся на группы посредством имеющегося обёртывающего планировщика (warp scheduler). Для достижения полного преимущества внутренне присущего параллелизма существующих SM и потоков одна и та же обёртка обязана выполнять одну и ту же инструкцию. Когда такое условие не выполняется, мы говорим о расхождении потоков (threads divergence).

Дополнительно

Полное руководство по программированию PyCUDA доступно на следующем сайте

Для установки PyCUDA в Windows 10, взгляните на следующую ссылку

Реализация управления памятью с применением PyCUDA

Программы PyCUDA обязаны соблюдать предписанные установленной структурой и внутренней организацией SM правила, которые налагают ограничения на производительность потоков. На практике знание и правильное применение различных видов предоставляемых GPU памяти имеют основополагающее значение для достижения максимальной эффективности. В таких GPU, в которых включено использование CUDA, существует четыре следующих вида памяти:

Регистры: Каждому потоку назначается некий регистр памяти, к которому имеет доступ только назначенный поток, даже если окружающие его потоки относятся к тому же самому блоку.

Разделяемая память: Каждый блок обладает своей собственной памятью, совместно используемой между относящимися к нему потоками. Даже эта память чрезвычайно быстрая.

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

Глобальная память: Все существующие в общей решётке потоки, а следовательно и все имеющиеся ядра, имеют доступ к общей памяти. Более того, данные продолжают оставаться там в точности как и в постоянной памяти:

Рисунок 8-4

Модель памяти GPU

Приготовление

Таким образом, для лучшей производительности некая программа PyCUDA должна максимально использовать все виды имеющейся памяти. В частности она должна максимально применять общую память, сводя к минимуму доступ к глобальной памяти.

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

Вот основные шаги, которым следует придерживаться всем потокам:

Загрузить данные из глобальной памяти в разделяемую память.

Синхронизовать все потоки определённого блока с тем, чтобы все они могли считывать безопасные позиции из разделяемой памяти, заполняемых прочими потоками.

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

Запись полученных результатов в глобальную память.

Для прояснения данного подхода, в своём следующем разделе мы представим некий пример, основывающийся на вычислении произведения двух матриц.

Как это сделать...

Следующий фрагмент кода показывает вычисление произведения двух матриц, M×N, стандартным методом, который основывается на последовательном подходе. Каждый элемент в получаемой в выводе матрице, P, получается после выборки элементов строки из матрицы M и элементов колонки из матрицы N:

void SequentialMatrixMultiplication(float*M,float *N,float *P, int width){
  for (int i=0; i< width; ++i)
      for(int j=0;j < width; ++j) {
          float sum = 0;
          for (int k = 0 ; k < width; ++k) {
              float a = M[I * width + k];
              float b = N[k * width + j];
              sum += a * b;
                     }
         P[I * width + j] = sum;
    }
}
P[I * width + j] = sum;

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

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

Самый первый шаг состоит в загрузке всех необходимых для реализации этого алгоритма модулей:

import numpy as np
from pycuda import driver, compiler, gpuarray, tools

Далее инициализируем своё устройство GPU:

import pycuda.autoinit

Мы реализуем kernel_code_template, который воплощает получение произведения двух матриц, соответственно обозначаемых как a и b, в то время как получаемая в результате матрица обозначается значением параметра c. Обратите внимание, что значение параметра MATRIX_SIZE будет задано на следующем шаге:

kernel_code_template = """
__global__ void MatrixMulKernel(float *a, float *b, float *c)
{
    int tx = threadIdx.x;
    int ty = threadIdx.y;
    float Pvalue = 0;
    for (int k = 0; k < %(MATRIX_SIZE)s; ++k) {
        float Aelement = a[ty * %(MATRIX_SIZE)s + k];
        float Belement = b[k * %(MATRIX_SIZE)s + tx];
        Pvalue += Aelement * Belement;
    }
    c[ty * %(MATRIX_SIZE)s + tx] = Pvalue;
}"""

наш следующий параметр будет применён для установки значения размерностей наших матриц. В данном случае размером будет 5x5:

MATRIX_SIZE = 5

Мы задаём две входных матрицы, a_cpu и b_cpu, которые будут содержать случайным образом присваиваемые значения с плавающей точкой:

a_cpu = np.random.randn(MATRIX_SIZE,
MATRIX_SIZE).astype(np.float32)
b_cpu = np.random.randn(MATRIX_SIZE,
MATRIX_SIZE).astype(np.float32)

После этого вычисляем произведение двух этих матриц, a и b, в своём устройстве хоста:

c_cpu = np.dot(a_cpu, b_cpu)

Мы выделяем облать памяти вычислительного устройства (GPU), равную размеру входных матриц:

a_gpu = gpuarray.to_gpu(a_cpu)
b_gpu = gpuarray.to_gpu(b_cpu)

Выделяем некую область памяти в своём GPU, равную по размеру выходной матрице результата произведения двух своих матриц. В данном случае наша результирующая матрица, c_gpu, имеет размер 5х5:

c_gpu = gpuarray.empty((MATRIX_SIZE, MATRIX_SIZE), np.float32)

Наш следующий код kernel_code повторно определяет kernel_code_template, но на этот раз с установленным размером matrix_size:

kernel_code = kernel_code_template % {
    'MATRIX_SIZE': MATRIX_SIZE}

Соответствующая директива SourceModule сообщает nvcc (NVIDIA CUDA Compiler) что ему придётся создать некий модуль - который представляет набор функций - содержащих ранее определённый kernel_code:

mod = compiler.SourceModule(kernel_code)

Наконец, мы получаем из этого модуля, mod, необходимую функцию MatrixMulKernel, которой мы присваиваем название matrixmul:

matrixmul = mod.get_function("MatrixMulKernel")

Мы выполняем произведение двух матриц, a_gpu и b_gpu, получая в результате матрицу c_gpu. Значение размера блока потоков определяется как MATRIX_SIZE, MATRIX_SIZE, 1:

matrixmul(
    a_gpu, b_gpu,
    c_gpu,
    block = (MATRIX_SIZE, MATRIX_SIZE, 1))

Выводим на печать входные матрицы:

print ("-" * 80)
print ("Matrix A (GPU):")
print (a_gpu.get())
print ("-" * 80)
print ("Matrix B (GPU):")
print (b_gpu.get())
print ("-" * 80)
print ("Matrix C (GPU):")
print (c_gpu.get())

Чтобы убедиться в правильности вычисления, выполненного в нашем GPU, мы проводим сравнение полученных результатов двух своих реализаций, одна из которых выполнена в вычислительном устройстве хоста (ЦПУ), а вторая осуществлена во внешнем вычислительном устройстве (GPU). Для этого мы воспользуемся директивой numpy allclose, которая поэлементно сличает два массива на равенство с точностью, равной 1e-05:

np.allclose(c_cpu, c_gpu.get())

Как это работает...

Давайте рассмотрим рабочий поток программирования PyCUDA. Подготовим необходимые входные матрицы, матрицы вывода и где мы будем хранить полученные результаты:

MATRIX_SIZE = 5
a_cpu = np.random.randn(MATRIX_SIZE, MATRIX_SIZE).astype(np.float32)
b_cpu = np.random.randn(MATRIX_SIZE, MATRIX_SIZE).astype(np.float32)
c_cpu = np.dot(a_cpu, b_cpu)

Затем мы перешлём эти матрицы в вычислительное устройство GPU воспользовавшись функцией PyCUDA gpuarray.to_gpu():

a_gpu = gpuarray.to_gpu(a_cpu)
b_gpu = gpuarray.to_gpu(b_cpu)
c_gpu = gpuarray.empty((MATRIX_SIZE, MATRIX_SIZE), np.float32)

Основной сердцевиной данного алгоритма является приводимая далее функция ядра. Давайте обратим внимание на имеющееся ключевое слово __global__, определяющее что данная функция является функцией ядра, что означает что она будет исполняться вычислительным устройством (GPU), вслед за вызовом из соответствующего кода хоста (ЦПУ):

__global__ void MatrixMulKernel(float *a, float *b, float *c){
    int tx = threadIdx.x;
    int ty = threadIdx.y;
    float Pvalue = 0;
    for (int k = 0; k < %(MATRIX_SIZE)s; ++k) {
        float Aelement = a[ty * %(MATRIX_SIZE)s + k];
        float Belement = b[k * %(MATRIX_SIZE)s + tx];
        Pvalue += Aelement * Belement;}
    c[ty * %(MATRIX_SIZE)s + tx] = Pvalue;
}

threadIdx.x и threadIdy.y это координаты, которые позволяют указывать конкретные потоки в имеющейся решётке двухмерных блоков. Обратите внимание, что такие потоки внутри установленной решётки блоков выполняют один и тот же код ядра, но с разными частями данных. Если мы сравним свою параллельную версию с последовательной, тогда мы немедленно заметим, что наши индексы циклов i и j были заменены на соответствующие индексыthreadIdx.x и threadIdy.y.

Это означает, что в нашей параллельной версии у нас будет иметься лишь одна итерация данного цикла. На практике наше ядро MatrixMulKernel будет выполнено некой решёткой с размерностью потоков 5 х 5.

Это условие отображается на следующей схеме:

Рисунок 8-5

Оптимизация решётки и блока потоков для данного примера

Затем мы проверяем полученное произведение просто сравнивая две полученные в результате матрицы:

np.allclose(c_cpu, c_gpu.get())

Получаемый вывод следующий:

C:\> python memManagementPycuda.py

---------------------------------------------------------------------
Matrix A (GPU):
[[ 0.90780383 -0.4782407 0.23222363 -0.63184392 1.05509627]
 [-1.27266967 -1.02834761 -0.15528528 -0.09468858 1.037099 ]
 [-0.18135822 -0.69884419 0.29881889 -1.15969539 1.21021318]
 [ 0.20939326 -0.27155793 -0.57454145 0.1466181 1.84723163]
 [ 1.33780348 -0.42343542 -0.50257754 -0.73388749 -1.883829 ]]
---------------------------------------------------------------------
Matrix B (GPU):
[[ 0.04523897 0.99969769 -1.04473436 1.28909719 1.10332143]
 [-0.08900332 -1.3893919 0.06948703 -0.25977209 -0.49602833]
 [-0.6463753 -1.4424541 -0.81715286 0.67685211 -0.94934392]
 [ 0.4485206 -0.77086055 -0.16582981 0.08478995 1.26223004]
 [-0.79841441 -0.16199949 -0.35969591 -0.46809086 0.20455229]]
---------------------------------------------------------------------
Matrix C (GPU):
[[-1.19226956 1.55315971 -1.44614291 0.90420711 0.43665022]
 [-0.73617989 0.28546685 1.02769876 -1.97204924 -0.65403283]
 [-1.62555301 1.05654192 -0.34626681 -0.51481217 -1.35338223]
 [-1.0040834 1.00310731 -0.4568972 -0.90064859 1.47408712]
 [ 1.59797418 3.52156591 -0.21708387 2.31396151 0.85150564]]
---------------------------------------------------------------------

TRUE

Также ознакомьтесь...

Размещённые в совместной памяти данные обладают ограниченной видимостью в однопоточном блоке. Легко обнаружить что представленная модель программирования PyCUDA адаптируется к конкретным классам приложений.

В частности, те свойства, которые должны представлять эти приложения связаны с наличием множества математических операций, причём с высокой степенью параллелизма (то есть одна и та же последовательность операций повторяется на больших объёмах данных).

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

Дополнительно

Дополнительные примеры использования PyCUDA можно обнаружить по следующей ссылке.

Введение в PyOpenCL

PyOpenCL является братским проектом по отношению к PyCUDA. Это библиотека привязок, которая предоставляет полный доступ к API OpenCL из Python, причём также от Андреаса Клёкнера. Он оснащён множеством тех же самых понятий что и PyCUDA, включая очистку от вышедших из сферы объектов, частичног абстрагирование от структур данных, а также обработку ошибок, причём всё это с минимумом накладных расходов. Этот проект доступен по лицензии MIT; его документация очень хороша и в открытом доступе можно найти обилие руководств и учебных пособий.

Основное средоточие PyOpenCL состоит в предоставлении некого обладающего лёгким весом соединения между Python и OpenCL, но он также содержит поддержку шаблонов и метапрограмм. Основной поток программы PyOpenCL почти в точности та же самая, что и у программ C и C++ для OpenCL. Сама программа хоста подготавливаем необходимые вызовы программ вычислительного устройства, запускает их, а затем дожидается полученных результатов.

Приготовление

Основной ссылкой для установки PyOpenCL является домашняя страница Андреаса Клёкнера.

Если вы применяете Anaconda, то она предлагает такие шаги:

Установите самый последний дистрибутив Anaconds c Python 3.7 со следующей ссылки. Для данного раздела была установлена через установщик Windows Anaconda 2019.07.

Со ссылки получите предварительно построенный двоичный файл PyOpenCL от Кристофера Гёльке. Выбеоите правильное сочетание версий ОС и CPython. В нашем случае применяется pyopencl-2019.1+cl12-cp37-cp37m-win_amd64.whl.

Для установки указанного выше пакета воспользуйтесь pip Просто в приглашении своей Anaconda наберите:

(base) C:\> pip install <directory>\pyopencl-2019.1+cl12-cp37-cp37m-win_amd64.whl

здесь <directory> это та папка, в которой расположен пакет PyOpenCL.

Более того, приводимая ниже нотация указывает на тот факт, что мы работаем из приглашения Anaconda:

(base) C:\>

здесь <directory> это та папка, в которой расположен пакет PyOpenCL.

Как это сделать...

В своём следующем примере мы воспользуемся некой функцией PyOpenCL, которая позволит нам перенумеровать все свойства того GPU, с которым она будет работать.

Необходимый для этого код очень прост и логичен:

Самым первым шагом мы импортируем необходимую нам библиотеку pyopencl:

import pyopencl as cl

Мы строим функцию, вывод которой предоставит нам все характеристики применяемого нами GPU:

def print_device_info() :
    print('\n' + '=' * 60 + '\nOpenCL Platforms and Devices')
    for platform in cl.get_platforms():
        print('=' * 60)
        print('Platform - Name: ' + platform.name)
        print('Platform - Vendor: ' + platform.vendor)
        print('Platform - Version: ' + platform.version)
        print('Platform - Profile: ' + platform.profile)

        for device in platform.get_devices():
            print(' ' + '-' * 56)
            print(' Device - Name: ' \
                  + device.name)
            print(' Device - Type: ' \
                  + cl.device_type.to_string(device.type))
            print(' Device - Max Clock Speed: {0} Mhz'\
                  .format(device.max_clock_frequency))
            print(' Device - Compute Units: {0}'\
                  .format(device.max_compute_units))
            print(' Device - Local Memory: {0:.0f} KB'\
                  .format(device.local_mem_size/1024.0))
            print(' Device - Constant Memory: {0:.0f} KB'\
                  .format(device.max_constant_buffer_size/1024.0))
            print(' Device - Global Memory: {0:.0f} GB'\
                  .format(device.global_mem_size/1073741824.0))
            print(' Device - Max Buffer/Image Size: {0:.0f} MB'\
                  .format(device.max_mem_alloc_size/1048576.0))
            print(' Device - Max Work Group Size: {0:.0f}'\
                  .format(device.max_work_group_size))
    print('\n')

Итак, мы реализуем свою функцию main, которая вызывает реализованную выше функцию print_device_info:

if __name__ == "__main__":
    print_device_info()

Как это работает...

Следующая команда применяется для импорта библиотеки pyopencl:

import pyopencl as cl

Это делает для нас доступным метод get_platforms, который возвращает список экземпляров платформы,то есть перечень устройств для данной системы:

for platform in cl.get_platforms():

Затем для каждого обнаруженного устройства отображаются следующие основные свойства:

Название и тип устройства

Максимальная тактовая частота

Вычислительные элементы

Локальная/ постоянная/ глобальная память

Для нашего примера вывод таков:

(base) C:\> python deviceInfoPyopencl.py

=============================================================
OpenCL Platforms and Devices
============================================================
Platform - Name: NVIDIA CUDA
Platform - Vendor: NVIDIA Corporation
Platform - Version: OpenCL 1.2 CUDA 10.1.152
Platform - Profile: FULL_PROFILE
    --------------------------------------------------------
    Device - Name: GeForce 840M
    Device - Type: GPU
    Device - Max Clock Speed: 1124 Mhz
    Device - Compute Units: 3
    Device - Local Memory: 48 KB
    Device - Constant Memory: 64 KB
    Device - Global Memory: 2 GB
    Device - Max Buffer/Image Size: 512 MB
    Device - Max Work Group Size: 1024
============================================================
Platform - Name: Intel(R) OpenCL
Platform - Vendor: Intel(R) Corporation
Platform - Version: OpenCL 2.0
Platform - Profile: FULL_PROFILE
    --------------------------------------------------------
    Device - Name: Intel(R) HD Graphics 5500
    Device - Type: GPU
    Device - Max Clock Speed: 950 Mhz
    Device - Compute Units: 24
    Device - Local Memory: 64 KB
    Device - Constant Memory: 64 KB
    Device - Global Memory: 3 GB
    Device - Max Buffer/Image Size: 808 MB
    Device - Max Work Group Size: 256
    --------------------------------------------------------
    Device - Name: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
    Device - Type: CPU
    Device - Max Clock Speed: 2400 Mhz
    Device - Compute Units: 4
    Device - Local Memory: 32 KB
    Device - Constant Memory: 128 KB
    Device - Global Memory: 8 GB
    Device - Max Buffer/Image Size: 2026 MB
    Device - Max Work Group Size: 8192

Также ознакомьтесь...

OpenCL в настоящее время управляется Khronos Group, некоммерческим консорциумом компаний, которые сотрудничают в определении спецификаций этого (и многих иных) стандартов и параметров совместимости для создания особенных для OpenCL драйверов всех видов платформ.

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

Дополнительные сведения по OpenCL можно найти по следующей ссылке.

Дополнительно

Документация PyOpenCL доступна здесь.

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

Построение приложений при помощи PyOpenCL

Самый первый шаг построения программы для PyOpenCL состоит в написании кода самого приложения хоста. Он выполняется на основном ЦПУ и обладает задачей управления всем возможным исполнением своего ядра в имеющейся карте GPU (то есть в вычислительном устройстве).

Некое ядро (kernel) является базовым элементом исполняемого кода, аналогичного функции C. Он может быть параллельным для данных или параллельным для задач. Однако замковым камнем PyOpenCL выступает сама эксплуатация параллелизма.

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

Программы могут вызываться из приложений. Мы имеем очереди исполнения, которые указывают установленный порядок, в котором исполняются ядра. Однако, в некоторых случаях их можно запускать не следуя первоначальному порядку.

Наконец, мы можем перечислить все основополагающие элементы для разработки некого приложения при помощи PyOpenCL:

Устройство (Device): Указывает на само оборудование, в котором должен выполняться необходимый код ядра. Обратите внимание, что приложение PyOpenCL может выполняться как в плате ЦПУ, так и в плате GPU (также как и PyCUDA), а также во встроенных устройствах, таких как FPGA (Field Programmable Gate Arrays, Программируемые пользователями вентильные матрицы).

Программа (Program): Это группа ядер, которая имеет установленную задачу планирования того какие ядра должны запускаться в вычислительном устройстве.

Ядро (Kernel: Это непосредственно код для выполнения в вычислительном устройстве. Некое ядро представляет собой подобную C функцию, что означает, что она может компилироваться в любом устройстве, поддерживающем драйверы PyOpenCL.

Очередь команд (Command queue): Это порядки ввыполнения ядер вычислительного устройства.

Контекст (Context): представляет собой группу устройств, которая позволяет устройствам принимать ядра и обмениваться данными.

Ниже приводится схема, отображающая как такая структура данных может работать в неком приложении хоста:

Рисунок 8-6

Модель программирования PyOpenCL

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

Как это сделать...

В своей следующей программе мы покажем все основные шаги построения некого приложения с помощью PyOpenCL: основная задача для выполнения состоит в суммировании двух векторов. Чтобы обладать читаемым выводом, мы рассмотрим два вектора, которые обладают 100 элементами: каждый i-й элемент получаемого вектора будет эквивалентен сумме i-го элемента vector_a с i-м элементом vector_b:

Давайте начнём с импорта необходимых библиотек:

import numpy as np
import pyopencl as cl
import numpy.linalg as la

Мы задаём устанавливаемый размер векторов для сложения следующим образом:

vector_dimension = 100

Здесь определяются наши входные векторы, vector_a и vector_b:

vector_a = np.random.randint(vector_dimension,size=vector_dimension)
vector_b = np.random.randint(vector_dimension,size=vector_dimension)

В этой последовательности мы определяем platform device, context и queue:

platform = cl.get_platforms()[1]
device = platform.get_devices()[0]
context = cl.Context([device])
queue = cl.CommandQueue(context)

Теперь пришло время организации областей в памяти, которые будут содержать наши входные векторы:

mf = cl.mem_flags
a_g = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,\ hostbuf=vector_a)
b_g = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,\ hostbuf=vector_b)

Наконец, мы строим необходимое ядро приложение с применением метода Program:

program = cl.Program(context, """
__kernel void vectorSum(__global const int *a_g, __global const int *b_g, __global int *res_g) {
  int gid = get_global_id(0);
  res_g[gid] = a_g[gid] + b_g[gid];
}
""").build()

Далее мы выделяем необходимую память для своей результирующей матрицы:

res_g = cl.Buffer(context, mf.WRITE_ONLY, vector_a.nbytes)

После этого мы вызываем необходимую функцию ядра:

program.vectorSum(queue, vector_a.shape, None, a_g, b_g, res_g)

Необходимое пространство памяти, применяемое для хранения получаемого результата, выделяется в области памяти основного хоста (res_np):

res_np = np.empty_like(vector_a)

Копируем полученный результат нашего вычисления м только что созданную область памяти:

cl._enqueue_copy(queue, res_np, res_g)

Наконец, выводим на печать полученные результаты:

print ("PyOPENCL SUM OF TWO VECTORS")
print ("Platform Selected = %s" %platform.name )
print ("Device Selected = %s" %device.name)
print ("VECTOR LENGTH = %s" %vector_dimension)
print ("INPUT VECTOR A")
print (vector_a)
print ("INPUT VECTOR B")
print (vector_b)
print ("OUTPUT VECTOR RESULT A + B ")
print (res_np)

За этим мы проводим простую проверку чтобы убедиться в том, что значение операции суммирования верное:

assert(la.norm(res_np - (vector_a + vector_b))) < 1e-5

Как это работает...

В своих приводимых ниже строках, после надлежащего импорта, мы определяем свои входные векторы:

vector_dimension = 100
vector_a = np.random.randint(vector_dimension, size= vector_dimension)
vector_b = np.random.randint(vector_dimension, size= vector_dimension)

Каждый вектор содержит 100 целых элементов, которые выбираются случайным образом посредством соответствующей функции numpy:

np.random.randint(max integer , size of the vector)

Далее мы выбираем необходимую платформу для получения своих вычислений, воспользовавшись методом get_platform():

platform = cl.get_platforms()[1]

После этого выбираем соответствующее устройство. В нашем случае, platform.get_devices()[0] соответствует графической карте Intel(R) HD Graphics 5500:

device = platform.get_devices()[0]

На своём следующем шаге определяются значения контекста и очереди; PyOpenCL предоставляет соответствующие методы контекста (выбранного устройства) и очереди (выбранного контекста):

context = cl.Context([device])
queue = cl.CommandQueue(context)

Для осуществления вычислений в выбранном устройстве, наш входной вектор копируется в память необходимого устройства:

mf = cl.mem_flags
a_g = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,\
hostbuf=vector_a)
b_g = cl.Buffer(context, mf.READ_ONLY | mf.COPY_HOST_PTR,\
 hostbuf=vector_b)

Далее мы подготавливаем необходимый буфер для получаемого в результате вектора:

res_g = cl.Buffer(context, mf.WRITE_ONLY, vector_a.nbytes)

Вот как задаётся код необходимого ядра:

program = cl.Program(context, """
__kernel void vectorSum(__global const int *a_g, __global const int *b_g, __global int *res_g) {
  int gid = get_global_id(0);
  res_g[gid] = a_g[gid] + b_g[gid];}
""").build()

Названием нашего ядра является vectorSum, а имеющийся список параметров определяет все типы данных входных аргументов и тип данных на выходе (оба являются векторами целых значений). Внутри самого тела ядра задаётся сумма двух векторов следующей последовательностью шагов:

Инициализируется значение индекса вектора: int gid = get_global_id(0).

Суммируются компоненты полученных векторов: res_g[gid] = a_g[gid] + b_g[gid].

В OpenCL (а следовательно и в PyOpenCL), значения буферов присоединяются к контексту (подробнее...), который перемещается в устройство, когда такой буфер применяется в этом устройстве.

Наконец, мы выполняем в своём устройстве vectorSum:

program.vectorSum(queue, vector_a.shape, None, a_g, b_g, res_g)

Для проверки полученного результата мы применяем оператор assert. Он Сверяет полученный результат и выставляет некую ошибку когда результат ложен:

assert(la.norm(res_np - (vector_a + vector_b))) < 1e-5

Получаемый вывод должен выглядеть как- то так:

(base) C:\> python vectorSumPyopencl.py

PyOPENCL SUM OF TWO VECTORS
Platform Selected = Intel(R) OpenCL
Device Selected = Intel(R) HD Graphics 5500
VECTOR LENGTH = 100
INPUT VECTOR A

[45 46 0 97 96 98 83 7 51 21 72 70 59 65 79 92 98 24 56 6 70 64 59 0
 96 78 15 21 4 89 14 66 53 20 34 64 48 20 8 53 82 66 19 53 11 17 39 11
 89 97 51 53 7 4 92 82 90 78 31 18 72 52 44 17 98 3 36 69 25 87 86 68
 85 16 58 4 57 64 97 11 81 36 37 21 51 22 17 6 66 12 80 50 77 94 6 70
 21 86 80 69]

INPUT VECTOR B
[25 8 76 57 86 96 58 89 26 31 28 92 67 47 72 64 13 93 96 91 91 36 1 75
  2 40 60 49 24 40 23 35 80 60 61 27 82 38 66 81 95 79 96 23 73 19 5 43
  2 47 17 88 46 76 64 82 31 73 43 17 35 28 48 89 8 61 23 17 56 7 84 36
 95 60 34 9 4 5 74 59 6 89 84 98 25 50 38 2 3 43 64 96 47 79 12 82
 72 0 78 5]

OUTPUT VECTOR RESULT A + B
[70 54 76 154 182 194 141 96 77 52 100 162 126 112 151 156 111 117 152
 97 161 100 60 75 98 118 75 70 28 129 37 101 133 80 95 91 130 58 74 134
 177 145 115 76 84 36 44 54 91 144 68 141 53 80 156 164 121 151 74 35
 107 80 92 106 106 64 59 86 81 94 170 104 80 76 92 13 61 69 171 70 87
 125 121 119 76 72 55 8 69 55 144 146 124 173 18 152 93 86 158 74]

Также ознакомьтесь...

В данном разделе мы рассмотрели модель выполнения PyOpenCL, которая как и PyCUDA, привлекает процесор хоста, управляющий одним или более разнородных устройств. В частности, каждая команда PyOpenCL отправляется в надлежащее устройство из своего хоста в виде исходного кода, который определяется посредством необходимой функции ядра.

Такой исходный код далее загружается в некий объект программы для эталонной архитектуры, эта программа компилируется для такой эталонной архитектуры, и создаётся надлежащий объект ядра, соотносимый с данной программой.

Объект ядра может выполняться в произвольном числе рабочих групп, создавая некую n- мерную вычислительную матрицу, что позволяет ему действенно подразделять общую рабочую нагрузку для задачи на n- измерений (1, 2 или 3) в каждой из рабочих групп. В свою очередь, они составляются в некое число исполнительных элементов, которые работают параллельно.

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

Ненадлежащая балансировка имеющейся рабочей нагрузки, совместно с конкретными особенностями каждого устройства (такими как задержки н обмен, пропускная способность и полоса пропускания) могут приводить к существенным потерям производительности или к компромиссам в присутствии переносимости вашего кода при выполнении без учёта сведений динамичного получения системы вычислений в плане вычислительных возможностей устройства.

Однако аккуратное применение таких технологий позволяет нам достигать высоких уровней производительности в сочетании с получаемыми результатами вычислений для различных вычислительных устройств.

Дополнительно

С дополнительными сведениями по программированию PyOpenCL можно ознакомиться по следующей ссылке.

Поэлементные выражения с применением PyOpenCL

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

Приготовление

Для поэлементной обработки в PyOpenCL реализуется метод ElementwiseKernel (context, argument, operation, name, optional_parameters) .

Его основные параметры таковы:

context является устройством или группой устройств в котором должна исполняться поэлементная обработка.

argument является неким C- подобным списком аргументов для всех вовлечённых в это вычисление параметров.

operation это строка, представляющая то действие, которое выполняется с заданным списком аргументов.

name выступает в качестве названия, которое ассоциируется с Elementwisekernel.

optional_parameters не важен для данного рецепта.

Как это сделать...

Здесь мы снова рассмотрим задачу сложения двух векторов.

Начинаем с импорта относящихся к делу библиотек:

import pyopencl as cl
import pyopencl.array as cl_array
import numpy as np

Задаём необходимые элемент контекста (context) и очередь команд (queue):

context = cl.create_some_context()
queue = cl.CommandQueue(context)

Здесь мы устанавливаем значение размерности вектора и выделяем необходимое пространство для своих входных и выходного векторов:

vector_dim = 100
vector_a=cl_array.to_device(queue,np.random.randint(100,\
size=vector_dim))
vector_b = cl_array.to_device(queue,np.random.randint(100,\
size=vector_dim))
result_vector = cl_array.empty_like(vector_a)

Мы устанавливаем elementwiseSum в качестве своего приложения ElementwiseKernel, а затем настраиваем для него некий набор аргументов, которые определяют те операции, которые следует применить для наших входных векторов:

elementwiseSum = cl.elementwise.ElementwiseKernel(context, "int *a,\
int *b, int *c", "c[i] = a[i] + b[i]", "sum")
elementwiseSum(vector_a, vector_b, result_vector)

Наконец, мы выводим на печать получаемый результат:

print ("PyOpenCL ELEMENTWISE SUM OF TWO VECTORS")
print ("VECTOR LENGTH = %s" %vector_dimension)
print ("INPUT VECTOR A")
print (vector_a)
print ("INPUT VECTOR B")
print (vector_b)
print ("OUTPUT VECTOR RESULT A + B ")
print (result_vector)

Как это работает...

В самых первых строках своего сценария мы импортируем все необходимые нам модули.

Для инициализации своего контекста мы применяем метод cl.create_some_context(). Он запрашивает у пользователя какой именно контекст требуется применять для данного вычисления:

Choose platform:
[0] <pyopencl.Platform 'NVIDIA CUDA' at 0x1c0a25aecf0>
[1] <pyopencl.Platform 'Intel(R) OpenCL' at 0x1c0a2608400>

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

queue = cl.CommandQueue(context)

Создаются экземпляры входных и выходного векторов. Наши входные векторы, vector_a и vector_b, являются векторами целочисленных значений, получаемых при помощи функции NumPy random.randint. Эти векторы, за этим, копируются в вычислительное усройство при помощи соответствующего оператора PyOpenCL:

cl.array_to_device(queue,array)

В ElementwiseKernel создаётся некий объект:

elementwiseSum = cl.elementwise.ElementwiseKernel(context,\
               "int *a, int *b, int *c", "c[i] = a[i] + b[i]", "sum")

[Совет]
Совет

Обратите внимание, что все необходимые аргументы представлены в виде строкового формата в качестве списка аргументов C (все они являются целочисленными).

Данная операция является неким фрагментом C-подобного кода, который несёт в себе необходимую операцию, а именно суммирование элементов наших входных векторов. Установленным названием для данной функции, которая будет нашим ядром,является sum.

Наконец, мы вызываем свою функцию elementwiseSum с заданными ранее значениями аргументов:

elementwiseSum(vector_a, vector_b, result_vector)

Данный пример завершается выводом на печать значений входных векторов и получаемого результата. Наш вывод выглядит как- то так:

(base) C:\>python elementwisePyopencl.py

Choose platform:
[0] <pyopencl.Platform 'NVIDIA CUDA' at 0x1c0a25aecf0>
[1] <pyopencl.Platform 'Intel(R) OpenCL' at 0x1c0a2608400>
Choice [0]:1

Choose device(s):
[0] <pyopencl.Device 'Intel(R) HD Graphics 5500' on 'Intel(R) OpenCL' at 0x1c0a1640db0>
[1] <pyopencl.Device 'Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz' on 'Intel(R) OpenCL' at 0x1c0a15e53f0>
Choice, comma-separated [0]:0
PyOpenCL ELEMENTWISE SUM OF TWO VECTORS
VECTOR LENGTH = 100
INPUT VECTOR A
[24 64 73 37 40 4 41 85 19 90 32 51 6 89 98 56 97 53 34 91 82 89 97 2
 54 65 90 90 91 75 30 8 62 94 63 69 31 99 8 18 28 7 81 72 14 53 91 80
 76 39 8 47 25 45 26 56 23 47 41 18 89 17 82 84 10 75 56 89 71 56 66 61
 58 54 27 88 16 20 9 61 68 63 74 84 18 82 67 30 15 25 25 3 93 36 24 27
 70 5 78 15]

INPUT VECTOR B
[49 18 69 43 51 72 37 50 79 34 97 49 51 29 89 81 33 7 47 93 70 52 63 90
 99 95 58 33 41 70 84 87 20 83 74 43 78 34 94 47 89 4 30 36 34 56 32 31
 56 22 50 52 68 98 52 80 14 98 43 60 20 49 15 38 74 89 99 29 96 65 89 41
 72 53 89 31 34 64 0 47 87 70 98 86 41 25 34 10 44 36 54 52 54 86 33 38
 25 49 75 53]

OUTPUT VECTOR RESULT A + B
[73 82 142 80 91 76 78 135 98 124 129 100 57 118 187 137 130 60 81 184
 152 141 160 92 153 160 148 123 132 145 114 95 82 177 137 112 109 133
 102 65 117 11 111 108 48 109 123 111 132 61 58 99 93 143 78 136 37 145
 84 78 109 66 97 122 84 164 155 118 167 121 155 102 130 107 116 119 50
 84 9 108 155 133 172 170 59 107 101 40 59 61 79 55 147 122 57 65
 95 54 153 68]

Также ознакомьтесь...

PyCUDA также обладает поэлементной функциональностью:

cl.array_to_device(queue,array)

Эта функциональность обладает во многом совпадающими аргументами со встроенной в PyOpenCL, за исключением параметра контекста. Тот же самый пример что и в данном разделе, который реализован для PyCUDA имеет такой листинг:

import pycuda.autoinit
import numpy
from pycuda.elementwise import ElementwiseKernel
import numpy.linalg as la

vector_dimension=100
input_vector_a = np.random.randint(100,size= vector_dimension)
input_vector_b = np.random.randint(100,size= vector_dimension)
output_vector_c = gpuarray.empty_like(input_vector_a)

elementwiseSum = ElementwiseKernel(" int *a, int * b, int *c",\
                             "c[i] = a[i] + b[i]"," elementwiseSum ")
elementwiseSum(input_vector_a, input_vector_b,output_vector_c)

print ("PyCUDA ELEMENTWISE SUM OF TWO VECTORS")
print ("VECTOR LENGTH = %s" %vector_dimension)
print ("INPUT VECTOR A")
print (vector_a)
print ("INPUT VECTOR B")
print (vector_b)
print ("OUTPUT VECTOR RESULT A + B ")
print (result_vector)

Дополнительно

По данной ссылке вы обнаружите занимательные примеры приложений PyOpenCL.

Оценка приложений PyOpenCL

В данном разделе мы выполняем сопоставительное тестирование производительности между ЦПУ и GPU с применением библиотеки PyOpenCL.

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

Приготовление

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

В своём следующем примере мы выполним некую проверку для наблюдения производительности в такой системе:

GPU: GeForce 840 M

ЦПУ: Intel Core i7 – 2.40 ГГц

ОЗУ: 8 ГБ

Как это сделать...

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

Наша первая функция выполняется только в имеющемся ЦПУ, в то время как вторая функция написана с применением PyOpenCL для использования установленной карты GPU. Данный тест выполняется для вектора с размером в 10 000 элементов.

Вот наш код:

Импортируем относящиеся к делу библиотеки. Обратите внимание на импорт библиотеки time для вычисления значения времени вычисления, а также библиотеки linalg, которая является неким инструментом инструментария линейной алгебры из библиотеки numpy:

from time import time
import pyopencl as cl
import numpy as np
import deviceInfoPyopencl as device_info
import numpy.linalg as la

Затем мы определяем значения входных векторов. Они оба содержат 10000 случайных элементов чисел с плавающей точкой:

a = np.random.rand(10000).astype(np.float32)
b = np.random.rand(10000).astype(np.float32)

Наша следующая функция вычисляет значение суммы двух векторов обработанное самим ЦПУ (хостом):

def test_cpu_vector_sum(a, b):
    c_cpu = np.empty_like(a)
    cpu_start_time = time()
    for i in range(10000):
            for j in range(10000):
                    c_cpu[i] = a[i] + b[i]
    cpu_end_time = time()
    print("CPU Time: {0} s".format(cpu_end_time - cpu_start_time))
    return c_cpu

Ещё одна функция вычисляет значение суммы двух векторов как результат работы GPU (устройства):

def test_gpu_vector_sum(a, b):
    platform = cl.get_platforms()[0]
    device = platform.get_devices()[0]
    context = cl.Context([device])
    queue = cl.CommandQueue(context,properties=\
                           cl.command_queue_properties.PROFILING_ENABLE)

Внутри своей функции test_gpu_vector_sum мы подготавливаем буферы в памяти для помещения в них значений входных и выходного векторов:

    a_buffer = cl.Buffer(context,cl.mem_flags.READ_ONLY \
                | cl.mem_flags.COPY_HOST_PTR, hostbuf=a)
    b_buffer = cl.Buffer(context,cl.mem_flags.READ_ONLY \
                | cl.mem_flags.COPY_HOST_PTR, hostbuf=b)
    c_buffer = cl.Buffer(context,cl.mem_flags.WRITE_ONLY, b.nbytes)

Всё ещё внутри своей функции test_gpu_vector_sum мы определяем необходимое ядро, которое будет вычислять значение суммы двух векторов в вычислительном устройстве:

    program = cl.Program(context, """
    __kernel void sum(__global const float *a,\
                      __global const float *b,\
                      __global float *c){
        int i = get_global_id(0);
        int j;
        for(j = 0; j < 10000; j++){
            c[i] = a[i] + b[i];}
    }""").build()

Далее мы сбрасываем значение переменной gpu_start_time перед началом вычислений. После этого мы вычисляем значение суммы двух векторов, а потом делаем оценку времени вычисления:

    gpu_start_time = time()
    event = program.sum(queue, a.shape, None,a_buffer, b_buffer,\
            c_buffer)
    event.wait()
    elapsed = 1e-9*(event.profile.end - event.profile.start)
    print("GPU Kernel evaluation Time: {0} s".format(elapsed))
    c_gpu = np.empty_like(a)
    cl._enqueue_read_buffer(queue, c_buffer, c_gpu).wait()
    gpu_end_time = time()
    print("GPU Time: {0} s".format(gpu_end_time - gpu_start_time))
    return c_gpu

И окончательно мы выполняем проверку, вызывая те две функции, которве были определены ранее:

if __name__ == "__main__":
    device_info.print_device_info()
    cpu_result = test_cpu_vector_sum(a, b)
    gpu_result = test_gpu_vector_sum(a, b)
    assert (la.norm(cpu_result - gpu_result)) < 1e-5

Как это работает...

Как пояснялось ранее, наш тест состоит в выполнении нашей задачи вычислений, причём обе в ЦПУ посредством функции test_cpu_vector_sum, а затем через установленное GPU при помощи функции test_gpu_vector_sum.

Обе функции выдают отчёт о времени выполнения.

Что касается функции проверки на ЦПУ, test_cpu_vector_sum, она состоит из двух вычислительных циклов по 10000 элементам векторов:

cpu_start_time = time()
               for i in range(10000):
                         for j in range(10000):
                             c_cpu[i] = a[i] + b[i]
               cpu_end_time = time()

Значение общего времени ЦПУ составляет разницу между следующим:

CPU Time = cpu_end_time - cpu_start_time

Что касается функции test_gpu_vector_sum, просматривая ядро выполнения вы можете видеть следующее:

__kernel void sum(__global const float *a,
                      __global const float *b,
                      __global float *c){
        int i=get_global_id(0);
        int j;
        for(j=0;j< 10000;j++){
            c[i]=a[i]+b[i];}

Значение суммы двух векторов выполняется через единственный цикл вычислений.

Полученный результат, как и можно было ожидать, состоит в значительном снижении во времени вычисления для функции test_gpu_vector_sum:

(base) C:\> python testApplicationPyopencl.py

============================================================
OpenCL Platforms and Devices
============================================================
Platform - Name: NVIDIA CUDA
Platform - Vendor: NVIDIA Corporation
Platform - Version: OpenCL 1.2 CUDA 10.1.152
Platform - Profile: FULL_PROFILE
    --------------------------------------------------------
    Device - Name: GeForce 840M
    Device - Type: GPU
    Device - Max Clock Speed: 1124 Mhz
    Device - Compute Units: 3
    Device - Local Memory: 48 KB
    Device - Constant Memory: 64 KB
    Device - Global Memory: 2 GB
    Device - Max Buffer/Image Size: 512 MB
    Device - Max Work Group Size: 1024
============================================================
Platform - Name: Intel(R) OpenCL
Platform - Vendor: Intel(R) Corporation
Platform - Version: OpenCL 2.0
Platform - Profile: FULL_PROFILE
    --------------------------------------------------------
    Device - Name: Intel(R) HD Graphics 5500
    Device - Type: GPU
    Device - Max Clock Speed: 950 Mhz
    Device - Compute Units: 24
    Device - Local Memory: 64 KB
    Device - Constant Memory: 64 KB
    Device - Global Memory: 3 GB
    Device - Max Buffer/Image Size: 808 MB
    Device - Max Work Group Size: 256
    --------------------------------------------------------
    Device - Name: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz
    Device - Type: CPU
    Device - Max Clock Speed: 2400 Mhz
    Device - Compute Units: 4
    Device - Local Memory: 32 KB
    Device - Constant Memory: 128 KB
    Device - Global Memory: 8 GB
    Device - Max Buffer/Image Size: 2026 MB
    Device - Max Work Group Size: 8192

CPU Time: 39.505873918533325 s
GPU Kernel evaluation Time: 0.013606592 s
GPU Time: 0.019981861114501953 s

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

Также ознакомьтесь...

OpenCL является стандартным крос- платформенным API для разработки приложений, которые эксплуатируют параллельное вычисление в неоднородных системах. Примечательна аналогия с CUDA, включая всё, начиная с имеющейся иерархии памяти, вплоть до непосредственного соответствия между потоками и элементами исполнения.

Даже на самом уровне программирования имеется множество аналогичных сторон и расширений с одной и той же функциональностью.

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

Благодаря большей зрелости и узкой специализации аппаратных средств, CUDA предлагает более простое управление устройством и API более высокого уровня, которые делают его более предпочтительным, но только когда вы имеете дело с конкретными архитектурами (то есть графическими картами nVidia).

В своих последующих разделах мы поясняем за и против библиотек CUDA и OpenCL, а также PyCUDA и PyOpenCL.

За OpenCL и PyOpenCL

Аргументы за таковы:

Они делают возможным использование неоднородных систем различного типа микропроцессоров.

Один и тот же код исполняется в различных системах.

Против OpenCL и PyOpenCL

Аргументы против таковы:

Сложное управление устройством

API не полностью стабилен

За CUDA и PyCUDA

Аргументы за такие:

API с очень высокими уровнями абстракции

Расширения для очень большого числа языков программирования

Гигантские объёмы документации и очень большое сообщество

Против CUDA и PyCUDA

Аргументы против такие:

Поддерживает только самые последние GPU nVidia ы качестве устройств

Снижает неоднородность ЦПУ и GPU

Дополнительно

Анлреас Клёкнер сделал ряд лекций по программированию GPU пр помощи PyCuda и PyOpenCL, доступные по ссылкам в ​www.​bu.​edu и www.​youtube.​com.

Программирование GPU с применением Numba

Numba является компилятором Python, который предоставляет основанный на CUDA API. Он разработан в первую очередь для задания численных вычислений, точно также как и библиотека NumPy. В частности, библиотека numba управляет типы массивов данных, предоставляемых NumPy и обрабатывает их.

Действительно, такая эксплуатация параллелизма данных, которая является присущей численным вычислениям с привлечением массивов, является естественным выбором для ускорителей GPU.

Компилятор работает с особыми типами сигнатур (или декораторов) для функций Python и делает возможной компиляцию времени исполнения (такой тип компиляции также имеет название Как раз вовремя, JiT - Just In Time).

Наиболее важными декораторами являются следующие:

jit: Позволяет разработчику писать CUDA- подобные функции. Когда они встречаются, имеющийся компилятор транслирует весь код под декоратором в язык псевдоассемблера PTX с тем, чтобы иметь возможность выполнения в GPU.

autojit: Снабжает комментарием функцию для процедуры отложенной компиляции, что означает, что такая функция с этой сигнатурой компилируется всего однажды.

vectorize: Создаёт так называемую ufunc (NumPy Universal Function), которая получает некую функцию и выполняет её параллельно для векторных аргументов.

guvectorize: Строит так называемую gufunc (NumPy Generalized Universal Function). Объект gufunc может работать с целыми подмассивами.

Приготовление

Numba (выпуск 0.45) совместим с Python 2.7 и 3.5 или более поздними версиями, а также с версиями NumPy с 1.7 до 1.16.

Для установки numba рекомендуется, как и для pyopencl, применять инфраструктуру Anaconda, а потому в приглашении Anaconda просто наберите следующее:

(base) C:\> conda install numba

Кроме того, для применения всего потенциала numba следует установить библиотеку cudatoolkit:

(base) C:\> conda install cudatoolkit

После этого можно проверить будут ли определяться как положено библиотека CUDA и GPU.

Из приглашения Anaconda откройте интерпретатор Python:

(base) C:\> python
Python 3.7.3 (default, Apr 24 2019, 15:29:51) [MSC v.1915 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>

Самая первая проверка влечёт за собой проверку того установлена ли как положено библиотека CUDA (cudatoolkit):

>>> import numba.cuda.api
>>> import numba.cuda.cudadrv.libs
>>> numba.cuda.cudadrv.libs.test()

Последующий вывод показывает такое качество нашей установки, при котором все проверки возвращают положительный результат:

Finding cublas from Conda environment
 located at C:\Users\Giancarlo\Anaconda3\Library\bin\cublas64_10.dll
 trying to open library... ok
Finding cusparse from Conda environment
 located at C:\Users\Giancarlo\Anaconda3\Library\bin\cusparse64_10.dll
 trying to open library... ok
Finding cufft from Conda environment
 located at C:\Users\Giancarlo\Anaconda3\Library\bin\cufft64_10.dll
 trying to open library... ok
Finding curand from Conda environment
 located at C:\Users\Giancarlo\Anaconda3\Library\bin\curand64_10.dll
 trying to open library... ok
Finding nvvm from Conda environment
 located at C:\Users\Giancarlo\Anaconda3\Library\bin\nvvm64_33_0.dll
 trying to open library... ok
Finding libdevice from Conda environment
 searching for compute_20... ok
 searching for compute_30... ok
 searching for compute_35... ok
 searching for compute_50... ok
True

В своей второй проверке мы удостоверяем присутствие графической карты:

>>> numba.cuda.api.detect()

Полученный вывод показывает обнаружена ли графическая карта и поддерживается ли она:

Found 1 CUDA devices
id 0 b'GeForce 840M' [SUPPORTED]
                      compute capability: 5.0
                           pci device id: 0
                              pci bus id: 8
Summary:
        1/1 devices are supported
True

Как это сделать...

В данном примере мы предоставляем демонстрацию компилятора Numba с применением аннотации @guvectorize.

Наша задача состоит в умножении матриц:

Импортируем guvectorize из библиотеки numba а также модуль numpy:

from numba import guvectorize
import numpy as np

При помощи декоратора @guvectorize мы определяем ту функцию matmul, которая выполнит задачу умножения матриц:

@guvectorize(['void(int64[:,:], int64[:,:], int64[:,:])'],
             '(m,n),(n,p)->(m,p)')
def matmul(A, B, C):
    m, n = A.shape
    n, p = B.shape
    for i in range(m):
        for j in range(p):
            C[i, j] = 0
            for k in range(n):
                C[i, j] += A[i, k] * B[k, j]

Наша матрица имеет размер 10 × 10, в то время как значения элементов целые:

dim = 10
A = np.random.randint(dim,size=(dim, dim))
B = np.random.randint(dim,size=(dim, dim))

Наконец, мы вызываем свою функцию matmul для предварительно определённых входных матриц:

C = matmul(A, B)

Мы выводим на печать входные матрицы и получаемую в результате матрицу:

print("INPUT MATRIX A")
print(":\n%s" % A)
print("INPUT MATRIX B")
print(":\n%s" % B)
print("RESULT MATRIX C = A*B")
print(":\n%s" % C)

Как это работает...

Наш декоратор @guvectorize обрабатывает аргументы массивов, получая четыре аргумента для определения значения сигнатуры gufunc:

Первые три аргумента определяют значения типов данных для управления и массивы целых значений: void(int64[:,:], int64[:,:], int64[:,:]).

Последний аргумент @guvectorize описывает как манипулировать установленными размерностями матриц: (m,n),(n,p)->(m,p).

Затем определяется операция матричного умножения, где A и B выступают в роли входных матриц, а C это выходная матрица: A(m,n)* B(n,p) = C(m,p), где m, n и p являются размерностями матриц.

Произведение матриц выполняется посредством трёх циклов for совместно со значениями индексов матриц:

for i in range(m):
            for j in range(p):
                C[i, j] = 0
                for k in range(n):
                      C[i, j] += A[i, k] * B[k, j]

Для построения входных матриц с размерностью 10 × 10 применяется функция NumPy randint:

dim = 10
A = np.random.randint(dim,size=(dim, dim))
B = np.random.randint(dim,size=(dim, dim))

Наконец, для этих матриц в качестве аргументов вызывается наша функция matmul и выводится на печать получаемая в результате матрица C:

C = matmul(A, B)
print("RESULT MATRIX C = A*B")
print(":\n%s" % C)

Для выполнения этого примера наберите следующее:

(base) C:\> python matMulNumba.py

Приводятся результаты заданных двух матриц и полученную в результате их умножения матрицу:

INPUT MATRIX A
:
[[8 7 1 3 1 0 4 9 2 2]
 [3 6 2 7 7 9 8 4 4 9]
 [8 9 9 9 1 1 1 1 8 0]
 [0 5 0 7 1 3 2 0 7 3]
 [4 2 6 4 1 2 9 1 0 5]
 [3 0 6 5 1 0 4 3 7 4]
 [0 9 7 2 1 4 3 3 7 3]
 [1 7 2 7 1 8 0 3 4 1]
 [5 1 5 0 7 7 2 3 0 9]
 [4 6 3 6 0 3 3 4 1 2]]
INPUT MATRIX B
:
[[2 1 4 6 6 4 9 9 5 2]
 [8 6 7 6 5 9 2 1 0 9]
 [4 1 2 4 8 2 9 5 1 4]
 [9 9 1 5 0 5 1 1 7 1]
 [8 7 8 3 9 1 4 3 1 5]
 [7 2 5 8 3 5 8 5 6 2]
 [5 3 1 4 3 7 2 9 9 5]
 [8 7 9 3 4 1 7 8 0 4]
 [3 0 4 2 3 8 8 8 6 2]
 [8 6 7 1 8 3 0 8 8 9]]
RESULT MATRIX C = A*B
:
[[225 172 201 161 170 172 189 230 127 169]
 [400 277 289 251 278 276 240 324 295 273]
 [257 171 177 217 208 254 265 224 176 174]
 [187 130 116 117 94 175 105 128 152 114]
 [199 133 117 143 168 156 143 214 188 157]
 [180 118 124 113 152 149 175 213 167 122]
 [238 142 186 165 188 215 202 200 139 192]
 [237 158 162 176 122 185 169 140 137 130]
 [249 160 220 159 249 125 201 241 169 191]
 [209 152 142 154 131 160 147 161 132 137]]

Также ознакомьтесь...

Написание алгоритма для операции снижения ранга при помощи PyCUDA может быть достаточно сложным. Для этой цели Numba предоставляет декоратор @reduce для преобразования простых двоичных операций в понижающие ядра (reduction kernels).

Операции снижения ранга сводят набор значений к отдельному значению. Типичным примером понижающей операции является вычисление суммы всех элементов массива. в качестве примера рассмотрим следующий массив элементов: 1, 2, 3, 4, 5, 6, 7, 8.

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

Рисунок 8-7

Последовательное суммирование

Параллельный же алгоритм работает по следующей схеме:

Рисунок 8-8

Параллельное суммирование

Становится ясно, что последний имеет определённое преимущество в сокращении времени исполнения.

С помощью Numba и установленного декоратора @reduce мы способны написать некий алгоритм в несколько строк кода для значения параллельного суммирования некого массива целых в диапазоне от 1 до 10 000:

import numpy
from numba import cuda

@cuda.reduce
def sum_reduce(a, b):
    return a + b

A = (numpy.arange(10000, dtype=numpy.int64)) + 1
print(A)
got = sum_reduce(A)
print(got)

Наш предыдущий пример может быть выполнен после набора такой команды:

(base) C:\> python reduceNumba.py

Будет представлен такой результат:

vector to reduce = [ 1 2 3 ... 9998 9999 10000]
result = 50005000

Дополнительно

В этом репозитории вы можете обнаружить множество примеров Numba. Интересное введение в программирование Numba и CUDA можно найти по следующей ссылке.

Глава 9. Отладка и тестирование Python

Содержание

Глава 9. Отладка и тестирование Python

Что представляет собой отладка?

Что такое тестирование программного обеспечения?

Отладка при помощи Winpdb Reborn

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Взаимодействие через pdb

Приготовление

Взаимодействие через командную строку

Применение интерпретатора Python

Вставка директив в сам код для отладки

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Реализация rpdb для отладки

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Работа с unittest

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

Тестирование приложений при помощи nose

Приготовление

Как это сделать...

Как это работает...

Также ознакомьтесь...

Дополнительно

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

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

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

В этом контексте мы будем изучать три наиболее важных инструмента Python для отладки исполняемого кода. А именно, winpdb-reborn, который предлагает отладку в неком инструменте визуализации; pdb, отладчика из стандартной библиотеки Python; а также rpdb, в котором r это сокращение для слова удалённый (remote), что означает что это отладчик кода с некой удалённой машины.

Что касается тестирования программного обеспечения, мы изучим следующие инструменты: unittest и nose.

Это инфраструктуры для разработки тестирований элементов, при котором элемент это минимальный компонент некой программы внутри какой- то независимой операции.

В этой главе мы рассмотрим такие темы:

Что такое отладка?

Что такое тестирование программного обеспечения?

Отладка с применением Winpdb Reborn

Взаимодействие с pdb

Реализация rpdb для отладки

Работа с unittest

Проверка приложения с помощью nose

Что представляет собой отладка?

Термин отладка (debugging) указывает на деятельность по выявлению той части кода, в которой одна или более ошибок (багов) определены в программном обеспечении при его применении.

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

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

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

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

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

Рисунок 9-1

Фазы отладки
Выявление ошибки
Выявление того компонента, в котором находится ошибка
Выявление условия возникновения ошибки
Проектирование и реализация устранения ошибки

[Замечание]
Замечание

Конечно, Python предлагает своим разработчикам многочисленные средства отладки (обратитесь к списку отладчиков Python). В этой главе мы остановимся на Winpdb Reborn, rpdb и pdb.

Что такое тестирование программного обеспечения?

Как уже упоминалось во введении к данной главе, тестирование программного обеспечения является процессом, применяемым для выявления недостатков корректности, полноты и надёжности в разрабатываемом программном обеспечении.

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

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

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

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

Более того, тестирование элементов снижает общую стоимость - в плане времени и ресурсов - выявления и исправления недочётов, по сравнению с достижением того же самого результата выполняя проверки для всего приложения целиком.

Отладка при помощи Winpdb Reborn

Winpdb Reborn является одним из наиболее важных и широко известных отладчиков Python. Основной сильной стороной данного отладчика является управление процессом отладки кода на основе потока.

[Замечание]
Замечание

Winpdb Reborn основывается на отладчике RPDB2, в то время как Winpdb выступает интерфейсом GUI к RPDB2 (подробнее...).

Приготовление

Наиболее широко применяемым способом установки Winpdb Reborn (release 2.0.0 dev5) является установка через pip, поэтому в своей консоли вам требуется набрать следующее:

C:\> pip install winpdb-reborn

К тому же, если вы ещё не установили wxPython для своего дистрибутива Python, тогда вам следует установить и его. wxPython является кросс- платформным инструментарием GUI для языка программирования Python.

[Замечание]
Замечание

Для Python версий 2.x обратитесь к странице проекта. Для Python версий 3.xx, wxPython автоматически устанавливается в виде зависимости через pip.

В своём следующем разделе мы изучим основные свойства и графический интерфейс Winpdb Reborn посредством простого примера его использования.

Как это сделать...

Допустим, мы желаем проанализировать следующее приложение Pytthon, которое использует библиотеку потоков. Это некий пример, крайне близкий к тому, который мы уже описывали ранее в разделе Задание подкласса потока из Главы 2, Параллельность на основе потоков. В своём следующем примере мы применяем класс MyThreadClass для создания последовательности выполнения трех потоков и управления ими. Вот код для отладки целиком:

import time
import os
from random import randint
from threading import Thread

class MyThreadClass (Thread):
   def __init__(self, name, duration):
      Thread.__init__(self)
      self.name = name
      self.duration = duration
   def run(self):
      print ("---> " + self.name + \
             " running, belonging to process ID "\
             + str(os.getpid()) + "\n")
      time.sleep(self.duration)
      print ("---> " + self.name + " over\n")
def main():
    start_time = time.time()

    # Thread Creation
    thread1 = MyThreadClass("Thread#1 ", randint(1,10))
    thread2 = MyThreadClass("Thread#2 ", randint(1,10))
    thread3 = MyThreadClass("Thread#3 ", randint(1,10))

    # Thread Running
    thread1.start()
    thread2.start()
    thread3.start()

    # Thread joining
    thread1.join()
    thread2.join()
    thread3.join()

    # End
    print("End")

    #Execution Time
    print("--- %s seconds ---" % (time.time() - start_time))

if __name__ == "__main__":
    main()

Давайте рассмотрим следующие этапы:

Откроем свою консоль и наберём необходимое название в той папке, которая содержит этот файл примера, winpdb_reborn_code_example.py:

python -m winpdb .\winpdb_reborn_code_example.py

[Замечание]
Замечание

Это также работает и в macOS, однако вам потребуется воспользоваться неким построением инфраструктуры Python. Когда вы применяете Winpdb Reborn для Anaconda, просто пользуйтесь pythonw вместо python для запуска сеанса Winpdb Reborn.

В случае успешной установки должен открыться GUI Winpdb Reborn:

Рисунок 9-2

GUI Windpdb Reborn

Как мы можем видеть на следующем снимке экрана, мы вставили две точки прерывания (воспользовавшись меню Breakpoints), в обеих отображённых красным строках 12 и 23:

Рисунок 9-3

Точки прерывания кода

[Замечание]
Замечание

Для получения дополнительных сведений по точкам прерываний обратитесь к разделу Также ознакомьтесь... в этом рецепте.

Оставаясь в окне Source, мы помещаем свою мышку на строку 23, в которой мы поместили свою вторую точку прерывания и нажимаем клавишу F8, а затем на клавишу F5. Наша точка прерывания позволяет исполнить весь код вплоть до выбранной строки. Как вы можете видеть, Namespace указывает что мы рассматриваем значение своего экземпляра класса MyThreadClass с thread#1 в качестве аргумента:

Рисунок 9-4

Namespace

Другим основополагающим свойством этого отладчика является возможность Step Into, которая позволяет инспектировать не только отлаживаемый код, но ещё и библиотечные функции и вызываемые на исполнение подпрограммы.

Прежде чем вы начнёте удалять свои предыдущие точки прерывания (Menu | Breakpoints | Clear All), вставьте новую точку прерывания в строке 28:

Рисунок 9-5

Точка прерывания в строке 28

Наконец, нажмите клавишу F5 и ваше приложение будет выполнено вплоть до точки прерывания в строке 28.

Затем нажмите F7. Здесь ваше окно исходного кода больше не отображает код нашего примера, а вместо этого используемую нами библиотеку threading (смотрите снимок экрана внизу).

Тем самым, наша функциональность Breakpoints совместно с Step Into не только позволяет отлаживать сам код в запросе, но также делает возможным инспектирование всех библиотечных функций и всех применяемых подпрограмм:

Рисунок 9-6

Строка 28 окна исходного кода после выполнения Step Into

Как это работает...

В данном примере мы познакомились с инструментом Winpdb Reborn. Это среда отладки (как и все среды в целом) позволяет вам останавливать выполнение программы в точных пунктах, инспектировать стек выполнения, содержимое переменных, состояние созданных объектов и многое иное.

Чтобы пользоваться Winpdb Reborn, всего лишь выполните такие шаги:

Установите точки прерывания в своём исходном коде (в окне Source).

Инспектируйте необходимые функции через его функциональность Step Into .

Просматривайте состояние переменных (в окне Namespace) и стека выполнения (в окне Stack).

Наши точки прерывания устанавливаются простым двойным нажатием на нужную строку при помощи левой кнопки мыши (вы увидите выбранные строки окрашенными красным). В качестве общего предостережения предлагается не применять множество команд в одной и той же строке; в противном случае будет невозможно ассоциировать точки прерывания с одной из них.

Когда мы пользуемся правой кнопкой мыши, мы можем выбирать disable breakpoints (отключение точек прерывания) без их удаления (красная подсветка исчезнет). Для удаления всех точек прерывания вместо этого, воспользуйтесь командой Clear All, которая присутствует в меню Breakpoints, как это уже упоминалось ранее.

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

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

Просмотр Namespace отображает значение локальных переменных и позволяет вам инспектировать их значения. Когда такие переменные ссылаются на объекты, имеется возможность отыскать значение уникального идентификатора этого объекта и проинспектировать его состояние.

В целом выполнением программы можно управлять в различных режимах, связанных с соответствующей иконкой (или через клавиши Fx), присутствующей в полосе команд Winpdb Reborn.

Наконец, мы укажем следующие важные методы исполнения:

Step Into (клавиша F7): Возобновляет исполнение вашей программы по одной строке за раз, а также вызывает библиотечные методов или подпрограммы.

Return (клавиша F12): Позволяет вам продолжить выполнение в точности в той точке, в которой была активирована функция Step Into.

Next (клавиша F6): Возобновляет необходимое выполнение данной программы по одной строке за раз без остановки во всех вызываемых методах.

Run to Line (клавиша F8): Выполняет данную программу вплоть до её останова (в ожидании новых команд) до указанной строки.

Также ознакомьтесь...

Как мы уже видели в GUI Winpdb Reborn, наш подразделяется на пять основных окон:

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

Threads: Отображается текущий поток исполнения, а также описываются поля TID (сокращение от Thread ID), название этого потока и значение состояния данного потока.

Stack: Здесь отображается для анализа стек выполнения изучаемой программы. Стек также известен ккак структура данных LIFO ( Last In, First Out), поскольку самый последний вставленный в него элемент будет удаляться первым. Когда программа вызывает некую функцию, эта вызываемая функция должна знать как возвращать управление вызывавшей стороне, а потому значение адреса возврата вызываемой функции помещается в имеющемся стеке выполнения программы. Такой стек исполнения программы также содержит необходимую память для локальных переменных, применяемых при каждом вызове этой функции.

Console: Это интерфейс командной строки, который делает возможным текстовое взаимодействие между пользователем и Winpdb Reborn.

Source: Данное окно отображает исходный код отладки. Проматывая строки кода, также имеется возможность вставки точек прерывания нажатием F9 когда вы заинтересовались кодом в данной строке.

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

Существует множество стратегий отладки. Вот некоторые из них:

Воспроизведение ошибки: Выявление тех входных данных, которые приводят к ней.

Упрощение ошибки: Определение самых простых данных, вызывающих её.

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

Действую осознанно: В процессе отладки вы постоянно сравниваете текущие значения своих переменных с тем что ожидали обнаружить.

Проверяйте все подробности: Не упускайте подробности в процессе отладки. Будет лучше сделать некую пометку когда вы заметите какие- то расхождения в своём исходном коде.

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

Дополнительно

Хорошее руководство по Winpdb Reborn можно найти по следующей ссылке.

Взаимодействие через pdb

pdb является модулем Python для выполнения интерактивной отладки.

Основные функциональные возможности pdb таковы:

Применение контрольных точек

Интерактивная обработка исходного кода в построковом режиме

Анализ кадра стека

Данный отладчик реализуется посредством класса pdb. По этой причине его можно просто расширять новыми функциональными возможностями.

Приготовление

Никакая установка pdb не требуется, ибо он является частью самой стандартной библиотеки Python. Его можно запустить следуя таким основным шаблонам использования:

Взаимодействуя через имеющуюся командную строку

Применяя интерпретатор Python

Вставляя некие директивы (то есть операторы pdb) в свой код для отладки

Взаимодействие через командную строку

Самый простой метод, это просто передать название своей программы в качестве входного параметра. Например, для нашей программы pdb_test.py это выглядит так:

class Pdb_test(object):
    def __init__(self, parameter):
    self.counter = parameter

    def go(self):
        for j in range(self.counter):
             print ("--->",j)
        return

if __name__ == '__main__':
    Pdb_test(10).go()

При выполнении через командную строку, pdb загружает необходимый файл исходного кода для анализа и останавливает его выполнение в самом первом обнаруженном операторе. В этом случае наша отладка остановлена на строке 1 (то есть на определении самого класса Pdb_test):

python -m pdb pdb_test.py
> .../pdb_test.py(1)<module>()
-> class Pdb_test(object):
(Pdb)

Применение интерпретатора Python

Модуль pdb можно применять в интерактивном режиме при помощи команды run():

>>> import pdb_test
>>> import pdb
>>> pdb.run('pdb_test.Pdb_test(10).go()')
> <string>(1)<module>()
(Pdb)

В данном случае наш оператор run() взят из данного отладчика и остановит выполнение перед оценкой первого выражения.

Вставка директив в сам код для отладки

Для продолжительно работающих процессов, в которых проблема может возникать намного позднее при исполнении, будет гораздо более удобно начинать отладку в программе при помощи директивы pdb set_trace():

import pdb

class Pdb_test(object):
    def __init__(self, parameter):
        self.counter = parameter
    def go(self):
        for j in range(self.counter):
            pdb.set_trace()
            print ("--->",j)
        return

if __name__ == '__main__':
    Pdb_test(10).go()

set_trace() может вызываться из любой точки подлежащей отладке программы. Например, он может вызываться при возникновении некоторого условия, обработке исключительной ситуации или в конкретной ветви инструкций.

В данном случае получаемый вывод таков:

-> print ("--->",j)
(Pdb)

Выполнение кода останавливается, причём в точности вслед за выполнением оператора pdb.set_trace().

Как это сделать...

Для взаимодействия с pdb вам требуется применять его язык, который позволяет вам перемещаться по вашему коду и изменять необходимые значения ваших переменных, вставлять точки прерывания или перемещаться по вызовам в стеке:

Воспользуйтесь командой where (либо в качестве альтернативы её компактной формой, w) для того чтобы увидеть в какой строке кода идёт выполнение и значение стека вызова. В данном случае это строка 17 в методе go() из модуля pdb_test.py:

> python -m pdb pdb_test.py
-> class Pdb_test(object):
(Pdb) where
  c:\python35\lib\bdb.py(431)run()
-> exec(cmd, globals, locals)
  <string>(1)<module>()
(Pdb)

Проинспектируйте те строки кода, которые расположены рядом с текущим местоположением (указанным стрелкой) при помощи list. В установленном по умолчанию режиме вокруг текущей строки перечисляются 11 строк (пять до и пять после):

(Pdb) list
  1 -> class Pdb_test(object):
  2 def __init__(self, parameter):
  3 self.counter = parameter
  4
  5 def go(self):
  6 for j in range(self.counter):
  7 print ("--->",j)
  8 return
  9
 10 if __name__ == '__main__':
 11 Pdb_test(10).go()

Когда list получает два параметра, они интерпретируются как первая и последняя строки для отображения:

(Pdb) list 3,9
  3 self.counter = parameter
  4
  5 def go(self):
  6 for j in range(self.counter):
  7 print ("--->",j)
  8 return
  9

Для перемещения к более старым кадрам стека применяйте up (u), а для движения к последним кадрам в стеке down (d):

(Pdb) up
> <string>(1)<module>()
(Pdb) up
> c:\python35\lib\bdb.py(431)run()
-> exec(cmd, globals, locals)
(Pdb) down
> <string>(1)<module>()
(Pdb) down
>....\pdb_test.py(1)<module>()
-> class Pdb_test(object):
(Pdb)

Как это работает...

Отладка выполняется в соответствии с установленным потоком исполняемой программы (трассировка). В каждой строке кода, кодировщик отображает выполняемые инструкциями операции в режиме реального времени и те значения, которые записаны в установленных переменных. Таким образом разработчик имеет возможность проверять что всё работает верно и определять причину неисправности.

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

Также ознакомьтесь...

Наиболее полезные команды pdb, совместно с их краткими формами для того чтобы держать их в памяти при отладке Python таковы:

Таблица 9-1. Основные команды pdb

Команда
Действие

args

Выводит на печать список аргументов текущей функции

break

Создаёт точку прерывания (требуются параметры)

continue

Продолжает выполнение программы

help

Приводит перечень команд (или подсказку) для некой команды (или параметра)

jump

Устанавливает следующую строку для выполнения

list

Выводит на печать исходный код вокруг текущей строки

next

Продолжает выполнение пока не возникнет следующая строка в текущей функции или не будет выполнен возврат

step

Выполняет текущую строку, останавливаясь при первом возможном случае

pp

Улучшенный вывод на печать значения данного выражения

quit или exit

Прерывает pdb

return

Продолжает выполнение врлоть до возврата из текущей функции

Дополнительно

Для получения дополнительных сведений относительно pdb вы можете просмотреть занимательное видео- руководство.

Реализация rpdb для отладки

При некоторых обстоятельствах бывает более удобной отладка кода из удадённого местоположения, которое не расположено в той же самой машине, в которой запущен ваш отладчик. Для этой цели был разработан rpdb. Он является некой обёрткой для pdb, которая применяет сокет TCP для взаимодействия с внешним миром.

Приготовление

Установка rpdb вначале требует самого основного шага с применением pip. Для ОС Windows просто наберите следующее:

C:\> pip install rpdb

Далее вам следует убедиться, что у вас имеется работающим и включённым в вашей машине клиент telnet. В Windows 10 когда вы откроете приглашение Командной строки и наберёте telnet, ваша ОС ответит сообщением об ошибке, так как он не представлен при выполняемой по умолчанию установке.

Давайте рассмотрим как установить его за несколько протсых шагов:

Откройте приглашение Вомандной строки в режиме Администратора.

Кликните по кнопке поиска и наберите cmd.

В появившемся списке кликните правой кнопкой по элементу приглашения Командной строки и выберите Run as Administrator.

Затем при работе в качестве Администратора в приглашении Командной строки наберите следующую команду:

dism /online /Enable-Feature /FeatureName:TelnetClient

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

Рисунок 9-7

Теперь вы можете применять telnet непосредственно из этого приглашения. Набрав telnet вы получите появившимся такое окно:

Рисунок 9-8

В своём следующем примере мы рассмотрим как запускать удалённую отладку при помощи rpdb.

Как это сделать...

Давайте выполним такие шаги:

Рассмотрим такой образец кода:

import threading

def my_func(thread_number):
    return print('my_func called by thread N°
        {}'.format(thread_number))

def main():
    threads = []
    for i in range(10):
        t = threading.Thread(target=my_func, args=(i,))
        threads.append(t)
        t.start()
        t.join()

if __name__ == "__main__":
    main()

Для использования rpdb вам требуется вставить следующие строки кода (сразу вслед за оператором import threading). Фактически, эти три строки кода включают использование rpdb через порт 4444 удалённого клиента с IP адресом 127.0.0.1:

import rpdb
debugger = rpdb.Rpdb(port=4444)
rpdb.Rpdb().set_trace()

Если вы запустите наш образец кода после вставки этих трёх строк, который включили применение rpdb, тогда вы должны обнаружить в приглашении своей Командной строки следующее сообщение:

pdb is running on 127.0.0.1:4444

Затем вы можете переключиться на отладку этого образца кода удалённо осуществив такое подключение через telnet:

telnet localhost 4444

Должно открыться следующее окно:

Рисунок 9-9

В своём образце кода обратите внимание на строку 7. Этот код не выполняется, он всего лишь ожидает выполнения некой инструкции:

Рисунок 9-10

Например, в нашем случае мы выполняем этот код и всякий раз повторно набираем оператор next:

(Pdb) next
> c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py(10)<module>()
-> def main():
(Pdb) next
> c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py(18)<module>()
-> if __name__ == "__main__":
(Pdb) next
> c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py(20)<module>()
-> main()
(Pdb) next
my_func called by thread N 0
my_func called by thread N 1
my_func called by thread N 2
my_func called by thread N 3
my_func called by thread N 4
my_func called by thread N 5
my_func called by thread N 6
my_func called by thread N 7
my_func called by thread N 8
my_func called by thread N 9
--Return--
> c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py(20)<module>()->None
-> main()
(Pdb)

После того как наша программа завершится, вы всё ещё можете продолжать работать в разделе отладки. Теперь давайте в своём следующем разделе рассмотрим как работает rpdb.

Как это работает...

В нашем предыдущем разделе мы наблюдали как просто перемещаемся по своему коду при помощи оператора next, который продолжает исполнение вплоть до достижения следующей строки в текущей функции или возврата из неё.

Для применения rpdb применяйте следующие шаги:

Импортируйте библиотеку rpdb:

import rpdb

Задайте параметр debugger, который определяет значение порта telnet для подключения к исполнению этого отладчика:

debugger = rpdb.Rpdb(port=4444)

Вызовите директиву set_trace(), которая позволяет войти в необходимый режим отладчика:

rpdb.Rpdb().set_trace()

В нашем случае мы поместили директиву set_trace() сразу после своего экземпляра debugger. На практике мы можем помещать её в любом месте своего кода; например, когда удовлетворяется некое условие, или внутри некого раздела, управляющего какой- то исключительной ситуацией.

Наш второй шаг, вместо этого, состоит в открытии приглашения Командной строки и запуске telnet, с установкой того самого значения порта, которое определено при задании параметра debugger внутри образца нашего кода:

telnet localhost 4444

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

Также ознакомьтесь...

Полный перечень команд, при помощи которых вы можете взаимодействовать в rpdb может быть отображён через набор команды help в соответствующем приглашении Pdb:

> c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py(7)<module>()
-> def my_func(thread_number):
(Pdb) help

Documented commands (type help <topic>):
========================================
EOF   c   d   h list q rv undisplay
a cl debug help ll quit s unt
alias clear disable ignore longlist r source until
args commands display interact n restart step up
b condition down j next return tbreak w
break cont enable jump p retval u whatis
bt continue exit l pp run unalias where

Miscellaneous help topics:
==========================
pdb exec

(Pdb)

Помимо наиболее полезных команд эта справка также содержит сведения о том как мы можем вставлять точки прерывания в свой код:

Наберите b и значение строки для установки некой точки прерывания. В нашем случае точки прерывания устанавливаются в строках 5 и 10:

(Pdb) b 5
Breakpoint 1 at c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py:5
(Pdb) b 10
Breakpoint 2 at c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py:10

Для отображения всех реализованных точек прерывания достаточно просто набрать команду b:

(Pdb) b
Num Type Disp Enb Where
1 breakpoint keep yes at c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py:5
2 breakpoint keep yes at c:\users\giancarlo\desktop\python parallel programming cookbook 2nd edition\python parallel programming new book\chapter_x- code debugging\rpdb_code_example.py:10
(Pdb)

Всякой вновь добавляемой точке прерывания присваивается некий численный идентификатор. Он указывает нам применение включения, запрета и интерактивного удаления точек прерывания. Чтобы запретить точку прерывания воспользуйтесь командой disable, которая сообщит установленному отладчику о том чтобы не останавливаться по достижению данной строки. Эта точка прерывания не забывается, а просто игнорируется.

Дополнительно

На этом сайте вы можете найти сведения по pdb и далее по rpdb.

В своих двух последующих разделах мы рассмотрим некоторые инструментов Python, которые применяются для реализации тестирования элементов:

unittest

nose

Работа с unittest

Нужный нам модуль unittest предоставляется стандартной библиотекой Python. Она обладает обильным набором инструментов и процедур для выполнения тестирования элементов. В этом разделе мы кратко рассмотрим то как работает unittest.

Тестирование элемента состоит из двух частей:

Собственно код для управления так называемой тестовой системой

Тест сам по себе

Приготовление

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

Простейший модуль unittest может ьыть составлен следующим образом:

import unittest

class SimpleUnitTest(unittest.TestCase):

    def test(self):
        self.assertTrue(True)

if __name__ == '__main__':
    unittest.main()

Для запуска модуля unittest вам требуется включить unittest.main (), в то время как у нас имеется некий единственный метод, test(), который отказывает если True примет когда- либо значение False.

Запустив наш предыдущий пример, вы получите следующий результат:

-----------------------------------------------------------
Ran 1 test in 0.005s

OK

Наш тест был успешен, тем самым выдав нам надлежащий результат, OK.

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

Как это сделать...

Давайте рассмотрим как мы можем охарактеризовать результаты проверки на данном примере:

Импортируем относящийся к делу модуль:

import unittest

Определяем свой класс outcomesTest, который имеет подкласс TestCase в качестве своего аргумента:

class OutcomesTest(unittest.TestCase):

Самым первым определяемым нами методом является testPass:

def testPass(self):
        return

Вот наш метод TestFail:

def testFail(self):
    self.failIf(True)

Ещё у нас имеется метод TestError:

def testError(self):
        raise RuntimeError('test error!')

Наконец, мы обладаем функцией main, для которой мы повторно вызываем свою процедуру:

if __name__ == '__main__':
    unittest.main()

Как это работает...

В данном примере показаны возможные выдачи проверки модуля в результате unittest.

Вот все возможные выводы:

ERROR: Выполненный тест возбудил исключительную ситуацию AssertionError. Нет никакого способа в явном виде пройти проверку, поэтому состояние данного теста зависит от присутствия (или отсутствия) некой исключительной ситуации.

FAILED: Данная проверка не пройдена и возбуждена исключительная ситуация AssertionError.

OK: Проверка прошла успешно.

Наш вывод таков:

===========================================================
ERROR: testError (__main__.OutcomesTest)
-----------------------------------------------------------
Traceback (most recent call last):
  File "unittest_outcomes.py", line 15, in testError
    raise RuntimeError('Errore nel test!')
RuntimeError: Errore nel test!

===========================================================
FAIL: testFail (__main__.OutcomesTest)
-----------------------------------------------------------
Traceback (most recent call last):
  File "unittest_outcomes.py", line 12, in testFail
    self.failIf(True)
AssertionError

-----------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1, errors=1)

Большинство проверок подтверждают истинность условия. Имеются различные способы написания проверок, которые удостоверяются в истинности в зависимости от взгляда их автора на то, прошёл ли проверку желаемый результат данного кода. Когда такой код выдаёт результат, которое можно рассматривать как истинное, следует применять методы failUnless() и assertTrue(). Когда этот код выдаёт нечто ложное, тогда имеет смысл применять методы failIf() и assertFalse():

import unittest

class TruthTest(unittest.TestCase):

    def testFailUnless(self):
        self.failUnless(True)

    def testAssertTrue(self):
        self.assertTrue(True)

    def testFailIf(self):
        self.assertFalse(False)

    def testAssertFalse(self):
        self.assertFalse(False)

if __name__ == '__main__':
    unittest.main()

Получаем такие результаты:

> python unittest_failwithmessage.py -v
testFail (__main__.FailureMessageTest) ... FAIL

===========================================================
FAIL: testFail (__main__.FailureMessageTest)
-----------------------------------------------------------
Traceback (most recent call last):
  File "unittest_failwithmessage.py", line 9, in testFail
    self.failIf(True, 'Il messaggio di fallimento va qui')
AssertionError: Il messaggio di fallimento va qui

-----------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
robby@robby-desktop:~/pydev/pymotw-it/dumpscripts$ python unittest_truth.py -v
testAssertFalse (__main__.TruthTest) ... ok
testAssertTrue (__main__.TruthTest) ... ok
testFailIf (__main__.TruthTest) ... ok
testFailUnless (__main__.TruthTest) ... ok

-----------------------------------------------------------
Ran 4 tests in 0.000s

OK

Также ознакомьтесь...

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

Существуют обстоятельства, однако, при которых вы можете пожелать запустить некий тест для проверки того, что определённый код на самом деле производит какую- то исключительную ситуаций. Например, в тех случаях, когда в качестве некого атрибута для какого- то объекта передаётся неверное значение. В подобной ситуации failUnlessRaises() сделает ваш код более понятным чем перехват возникающей в вашем коде исключительной ситуации:

import unittest

def raises_error(*args, **kwds):
    print (args, kwds)
    raise ValueError\
        ('Valore non valido:'+ str(args)+ str(kwds))

class ExceptionTest(unittest.TestCase):
    def testTrapLocally(self):
        try:
            raises_error('a', b='c')
        except ValueError:
            pass
        else:
            self.fail('Non si vede ValueError')

    def testFailUnlessRaises(self):
       self.assertRaises\
               (ValueError, raises_error, 'a', b='c')

if __name__ == '__main__':
    unittest.main()

Результат для обеих функций один и тот же. Тем не менее, получаемый результат для второй проверки, которая пользуется failUnlessRaises(), короче:

> python unittest_exception.py -v
testFailUnlessRaises (__main__.ExceptionTest) ... ('a',) {'b': 'c'}
ok
testTrapLocally (__main__.ExceptionTest) ...('a',) {'b': 'c'}
ok

-----------------------------------------------------------
Ran 2 tests in 0.000s

OK

Дополнительно

Дополнительные сведения относительно тестирования Python можно найти по следующей ссылке.

Тестирование приложений при помощи nose

nose является важным модулем Python для задания поэлементных проверок. Он позволяет нам писать простые функции проверки при помощи подкласса unittest.TestCase, а также классы проверок, которые не являются подклассами unittest.TestCase.

Приготовление

Установите nose с помощью pip:

C:\> pip install nose

Его исходный пакет можно выгрузить со ссылки и установить при помощи таких шагов:

Раскрыть полученный исходный архив.

Выполнить cd для перехода в новый каталог.

После этого введите такую команду:

C:\> python setup.py install

Одной из сильных сторон nose является автоматический сбор проверок из следующего:

Исходных файлов Python

Обнаруженных в рабочей папке каталогов и пакетов

Для определения того какие проверки запускать, передайте соответствующие названия тестов в его командную строку:

C:\> nosetests only_test_this.py

Такие названия проверок можно определять по именам файлов или модулей, а также в качестве возможного варианта можно указывать тот случай, когда требуется запускать отдельный модуль или файл с указанным названием для случая названия теста с двоеточием. Имена файлов могут быть относительными или абсолютными.

Вот некоторые образцы:

C:\> nosetests test.module
C:\> nosetests another.test:TestCase.test_method
C:\> nosetests a.test:TestCase
C:\> nosetests /path/to/test/file.py:test_function

Вы также можете изменять рабочий каталог для которого nose ищет тесты при помощи переключателя -w:

C:\> nosetests -w /path/to/tests

Обратите внимание, тем не менее, что поддержка множества аргументов для -w считается обсуждаемой и будет удалена в последующих версиях. Тем не менее, имеется возможность получения того же самого поведения заданием целевых каталогов без переключателя -w:

C:\> nosetests /path/to/tests /another/path/to/tests

Последующая индивидуализация выбора проверок и загрузки возможна через использование подключаемых модулей (plugins).

Вывод результатов проверок идентичен тому что делает unittest, за исключением дополнительных функциональных возможностей, таких как классы ошибок и свойства поддержки подключаемых модулей, таких как перехват вывода и декларация самоанализа.

В своём следующем разделе мы рассмотрим некий класс проверок с использованием nose.

Как это сделать...

Давайте исполним следующие шаги:

Импортируем относящийся к делу nose.tools:

from nose.tools import eq_

Затем настроим класс TestSuite. В нашем случае все методы этого класса проверяются соответствующей функцией eq_:

class TestSuite:
    def test_mult(self):
        eq_(2*2,4)

    def ignored(self):
        eq_(2*2,3)

Как это работает...

Некая модульная проверка может разрабатываться независимо от его разработчика, однако рекомендуется иметь некий стандартный продукт, такой как unittest и придерживаться общей практики проверок.

Как вы можете видеть на следующем примере, нашим методом тестирования задана функция eq_. Она аналогична assertEquals из unittest, которая проверяет равенство двух параметров:

    def test_mult(self):
        eq_(2*2,4)

    def ignored(self):
        eq_(2*2,3)

Такая практика проверки, несмотря на наилучшие намерения, имеет очевидные ограничения, например, невозможность повторения по времени (скажем, при изменении программного модуля) для так называемых проверок регрессии.

Вот получаемый вывод:

C:\> nosetests -v testset.py
testset.TestSuite.test_mult ... ok

-----------------------------------------------------------
Ran 1 tests in 0.001s

OK

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

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

Также ознакомьтесь...

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

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

Дополнительно

Имеющее силу руководство для nose доступно по ссылке Главе 2,.

Указатель

Содержание

Указатель