Учебники

Параллелизм в Python — Краткое руководство

Параллелизм в Python — Введение

В этой главе мы поймем концепцию параллелизма в Python и узнаем о различных потоках и процессах.

Что такое параллелизм?

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

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

Исторический обзор параллелизма

Следующие пункты дадут нам краткий исторический обзор параллелизма —

Из концепции железных дорог

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

Параллельные вычисления в академических кругах

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

Параллельные примитивы высокого уровня

В последнее время программисты получают улучшенные параллельные решения из-за внедрения высокоуровневых примитивов параллелизма.

Улучшенный параллелизм с языками программирования

Языки программирования, такие как Google Golang, Rust и Python, сделали невероятные разработки в областях, которые помогают нам получать лучшие параллельные решения.

Что такое поток и многопоточность?

Поток — это самая маленькая единица выполнения, которая может быть выполнена в операционной системе. Это не сама программа, но она запускается внутри программы. Другими словами, потоки не являются независимыми друг от друга. Каждый поток разделяет раздел кода, раздел данных и т. Д. С другими потоками. Они также известны как легкие процессы.

Поток состоит из следующих компонентов —

  • Счетчик программ, состоящий из адреса следующей исполняемой инструкции

  • стек

  • Набор регистров

  • Уникальный идентификатор

Счетчик программ, состоящий из адреса следующей исполняемой инструкции

стек

Набор регистров

Уникальный идентификатор

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

пример

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

Что такое процесс и многопроцессорность?

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

Следующая диаграмма показывает различные этапы процесса —

многопроцессорная обработка

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

Многопроцессорная One

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

Мультипроцессор Два

Python — один из самых популярных языков программирования. Ниже приведены некоторые причины, которые делают его пригодным для одновременных приложений —

Синтаксический сахар

Синтаксический сахар — это синтаксис в языке программирования, предназначенный для облегчения чтения или выражения. Это делает язык «более сладким» для использования человеком: вещи могут быть выражены более четко, более кратко или в альтернативном стиле, основанном на предпочтениях. Python поставляется с методами Magic, которые могут быть определены для воздействия на объекты. Эти магические методы используются в качестве синтаксического сахара и связаны с более простыми для понимания ключевыми словами.

Большое Сообщество

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

Полезные API для параллельного программирования

Python 2 и 3 имеют большое количество API, предназначенных для параллельного / параллельного программирования. Наиболее популярными из них являются многопоточность, многопоточность, asyncio, gevent и greenlets и т. Д.

Ограничения Python при реализации параллельных приложений

Python имеет ограничение для одновременных приложений. Это ограничение называется GIL (Global Interpreter Lock) и присутствует в Python. GIL никогда не позволяет нам использовать несколько ядер CPU, и поэтому мы можем сказать, что в Python нет настоящих потоков. Мы можем понять концепцию GIL следующим образом —

GIL (глобальная блокировка интерпретатора)

Это одна из самых противоречивых тем в мире Python. В CPython GIL является мьютексом — блокировкой взаимного исключения, которая обеспечивает безопасность потоков. Другими словами, мы можем сказать, что GIL препятствует параллельному выполнению кода Python несколькими потоками. Блокировка может удерживаться только одним потоком за раз, и если мы хотим выполнить поток, он должен сначала получить блокировку. Диаграмма, показанная ниже, поможет вам понять работу GIL.

Ограничения

Однако в Python есть некоторые библиотеки и реализации, такие как Numpy, Jpython и IronPytbhon. Эти библиотеки работают без какого-либо взаимодействия с GIL.

Параллелизм против параллелизма

И параллелизм, и параллелизм используются по отношению к многопоточным программам, но существует много путаницы между сходством и различием между ними. Большой вопрос в этом отношении: параллелизм параллелизм или нет? Хотя оба термина выглядят довольно схожими, но ответ на поставленный выше вопрос — НЕТ, параллелизм и параллелизм не совпадают. Теперь, если они не одинаковы, то в чем их основная разница?

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

Параллельность в деталях

Параллельность — это когда две задачи перекрываются при выполнении. Это может быть ситуация, когда приложение выполняет более одной задачи одновременно. Мы можем понять это схематично; несколько задач достигают прогресса одновременно, а именно:

совпадение

Уровни параллелизма

В этом разделе мы обсудим три важных уровня параллелизма с точки зрения программирования:

Низкоуровневый параллелизм

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

Средний уровень параллелизма

В этом параллельном использовании нет явных атомарных операций. Он использует явные блокировки. Python и другие языки программирования поддерживают такой вид параллелизма. Главным образом прикладные программисты используют этот параллелизм.

Параллелизм высокого уровня

В этом параллелизме не используются ни явные атомарные операции, ни явные блокировки. Python имеет модуль concurrent.futures для поддержки такого рода параллелизма.

Свойства параллельных систем

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

Свойство правильности

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

Безопасность собственности

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

Живая собственность

Это свойство означает, что программа или система должны «прогрессировать», и она достигнет желаемого состояния.

Актеры параллельных систем

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

Ресурсы параллельных систем

Актеры должны использовать ресурсы, такие как память, диск, принтер и т. Д., Для выполнения своих задач.

Определенный набор правил

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

Барьеры параллельных систем

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

Обмен данными

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

Ограничение обмена данными

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

Помощь структуры данных

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

Неизменная передача данных

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

Изменяемая передача данных

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

Совместное использование ресурсов ввода / вывода

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

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

import urllib.request
import time
ts = time.time()
req = urllib.request.urlopen('http://www.tutorialspoint.com')
pageHtml = req.read()
te = time.time()
print("Page Fetching Time : {} Seconds".format (te-ts))

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

Выход

Page Fetching Time: 1.0991398811340332 Seconds

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

Что такое параллелизм?

Параллелизм может быть определен как искусство разделения задач на подзадачи, которые могут обрабатываться одновременно. Это противоположно параллелизму, как обсуждалось выше, в котором два или более события происходят одновременно. Мы можем понять это схематично; Задача разбита на несколько подзадач, которые могут обрабатываться параллельно, а именно:

параллелизм

Чтобы лучше понять различие между параллелизмом и параллелизмом, рассмотрим следующие моменты:

Параллельно, но не параллельно

Приложение может быть параллельным, но не параллельным, это означает, что оно обрабатывает более одной задачи одновременно, но задачи не разбиты на подзадачи.

Параллельно, но не одновременно

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

Ни параллельно, ни параллельно

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

Параллельно и параллельно

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

Необходимость параллелизма

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

Рассмотрим следующие важные моменты, чтобы понять, почему необходимо достичь параллелизма —

Эффективное выполнение кода

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

Быстрее, чем последовательные вычисления

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

Меньше времени выполнения

Параллельная обработка сокращает время выполнения программного кода.

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

Понимание процессоров для реализации

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

Одноядерные процессоры

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

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

Многоядерные процессоры

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

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

Цикл извлечения-декодирования-выполнения

Ядра многоядерных процессоров следуют циклу выполнения. Этот цикл называется циклом Fetch-Decode-Execute . Он включает в себя следующие шаги —

получать

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

раскодировать

Недавно полученные инструкции будут преобразованы в серию сигналов, которые будут запускать другие части ЦП.

казнить

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

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

Архитектура системы и памяти

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

Архитектуры компьютерных систем, поддерживающие параллелизм

Майкл Флинн в 1972 году дал таксономию для классификации различных стилей архитектуры компьютерной системы. Эта таксономия определяет четыре различных стиля следующим образом:

  • Один поток команд, один поток данных (SISD)
  • Один поток инструкций, несколько потоков данных (SIMD)
  • Поток с несколькими инструкциями, один поток данных (MISD)
  • Многократный поток команд, многократный поток данных (MIMD).

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

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

SSID

Преимущества СИСД

Преимущества архитектуры SISD следующие:

  • Это требует меньше энергии.
  • Нет проблем со сложным протоколом связи между несколькими ядрами.

Недостатки СИСД

Недостатки архитектуры SISD следующие:

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

Один поток инструкций, несколько потоков данных (SIMD)

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

SIMD

Лучший пример для SIMD — это видеокарты. Эти карты имеют сотни отдельных процессоров. Если говорить о разнице в вычислениях между SISD и SIMD, то для массивов добавления [5, 15, 20] и [15, 25, 10] архитектура SISD должна будет выполнить три различные операции добавления. С другой стороны, с архитектурой SIMD, мы можем добавить одну операцию добавления.

Преимущества SIMD

Преимущества SIMD-архитектуры следующие:

  • Одна и та же операция над несколькими элементами может быть выполнена с использованием только одной инструкции.

  • Пропускная способность системы может быть увеличена за счет увеличения количества ядер процессора.

  • Скорость обработки выше, чем у архитектуры SISD.

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

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

Скорость обработки выше, чем у архитектуры SISD.

Недостатки SIMD

Недостатки SIMD-архитектуры следующие:

  • Существует сложная связь между числами ядер процессора.
  • Стоимость выше, чем у архитектуры SISD.

Поток нескольких данных с одной инструкцией (MISD)

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

MISD

Представители архитектуры MISD еще не существуют коммерчески.

Поток нескольких данных с несколькими командами (MIMD)

В системе, использующей архитектуру MIMD, каждый процессор в многопроцессорной системе может выполнять разные наборы команд независимо от другого набора данных, установленного параллельно. Это противоположно архитектуре SIMD, в которой одна операция выполняется над несколькими наборами данных. Ниже приводится архитектура MIMD —

MIMD

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

Архитектуры памяти, поддерживающие параллелизм

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

UMA (унифицированный доступ к памяти)

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

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

UMA

Неоднородный доступ к памяти (NUMA)

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

NUMA

Архитектура кэш-памяти только (COMA)

Модель COMA является специализированной версией модели NUMA. Здесь все распределенные основные памяти преобразуются в кэш-память.

кома

Параллелизм в Python — Темы

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

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

Состояния потока

Чтобы глубже понять функциональность потоков, нам нужно узнать о жизненном цикле потоков или различных состояниях потоков. Как правило, поток может существовать в пяти различных состояниях. Различные состояния показаны ниже —

Новый поток

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

Runnable

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

Бег

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

Невыполняющийся / ожидание

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

мертв

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

Следующая диаграмма показывает полный жизненный цикл потока —

мертв

Типы ниток

В этом разделе мы увидим различные типы потоков. Типы описаны ниже —

Потоки уровня пользователя

Это управляемые пользователями потоки.

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

Примеры потоков уровня пользователя:

  • Потоки Java
  • POSIX темы

мертв

Преимущества потоков уровня пользователя

Ниже приведены различные преимущества потоков уровня пользователя —

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

Недостатки пользовательских тем

Ниже приведены различные недостатки потоков уровня пользователя —

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

Потоки уровня ядра

Управляемые потоки операционной системы действуют на ядро, которое является ядром операционной системы.

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

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

мертв

Преимущества потоков на уровне ядра

Ниже приведены различные преимущества потоков уровня ядра —

  • Ядро может одновременно планировать несколько потоков из одного процесса на несколько процессов.

  • Если один поток в процессе заблокирован, ядро ​​может запланировать другой поток того же процесса.

  • Сами подпрограммы ядра могут быть многопоточными.

Ядро может одновременно планировать несколько потоков из одного процесса на несколько процессов.

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

Сами подпрограммы ядра могут быть многопоточными.

Недостатки потоков на уровне ядра

  • Потоки ядра обычно медленнее создаются и управляются, чем пользовательские потоки.

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

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

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

Блок управления резьбой — TCB

Блок управления потоком (TCB) может быть определен как структура данных в ядре операционной системы, которая в основном содержит информацию о потоке. Информация о потоках, хранящаяся в TCB, выделяет некоторую важную информацию о каждом процессе.

Рассмотрим следующие моменты, связанные с темами, содержащимися в TCB —

  • Идентификация потока — это уникальный идентификатор потока (tid), назначенный каждому новому потоку.

  • Состояние потока — содержит информацию, относящуюся к состоянию (работает, работает, не работает, не работает) потока.

  • Счетчик программ (ПК) — указывает на текущую программную инструкцию потока.

  • Набор регистров — содержит значения регистров потока, назначенные им для вычислений.

  • Указатель стека — указывает на стек потока в процессе. Он содержит локальные переменные в области видимости потока.

  • Указатель на печатную плату — содержит указатель на процесс, который создал этот поток.

Идентификация потока — это уникальный идентификатор потока (tid), назначенный каждому новому потоку.

Состояние потока — содержит информацию, относящуюся к состоянию (работает, работает, не работает, не работает) потока.

Счетчик программ (ПК) — указывает на текущую программную инструкцию потока.

Набор регистров — содержит значения регистров потока, назначенные им для вычислений.

Указатель стека — указывает на стек потока в процессе. Он содержит локальные переменные в области видимости потока.

Указатель на печатную плату — содержит указатель на процесс, который создал этот поток.

печатная плата

Связь между процессом и нитью

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

В следующей таблице показано сравнение между процессом и потоком —

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

Концепция многопоточности

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

Понятие многопоточности можно понять с помощью следующего примера.

пример

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

Следующая диаграмма помогает нам понять, как в памяти существует несколько потоков:

Многопоточность

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

Плюсы многопоточности

Давайте теперь посмотрим на несколько преимуществ многопоточности. Преимущества заключаются в следующем —

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

  • Программа остается отзывчивой — она ​​позволяет программе оставаться отзывчивой, поскольку один поток ожидает ввода, а другой одновременно запускает графический интерфейс.

  • Доступ к глобальным переменным — в многопоточности все потоки определенного процесса могут получить доступ к глобальным переменным, и если есть какое-либо изменение в глобальной переменной, то это видно и другим потокам.

  • Использование ресурсов — запуск нескольких потоков в каждой программе улучшает использование процессора, а время простоя процессора уменьшается.

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

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

Программа остается отзывчивой — она ​​позволяет программе оставаться отзывчивой, поскольку один поток ожидает ввода, а другой одновременно запускает графический интерфейс.

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

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

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

Минусы многопоточности

Давайте теперь рассмотрим несколько недостатков многопоточности. Недостатки заключаются в следующем —

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

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

  • Увеличение сложности — многопоточность может увеличить сложность программы, и отладка становится сложной.

  • Привести в состояние тупика — многопоточность может привести к потенциальному риску достижения состояния тупика.

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

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

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

Увеличение сложности — многопоточность может увеличить сложность программы, и отладка становится сложной.

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

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

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

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

Модуль Python для реализации потоков

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

  • модуль <_thread>

  • модуль <threading>

модуль <_thread>

модуль <threading>

Основное различие между этими двумя модулями состоит в том, что модуль <_thread> обрабатывает поток как функцию, тогда как модуль <threading> обрабатывает каждый поток как объект и реализует его объектно-ориентированным способом. Кроме того, модуль <_thread> эффективен при низкоуровневой многопоточности и обладает меньшими возможностями, чем модуль <threading> .

модуль <_thread>

В более ранней версии Python у нас был модуль <thread>, но он долгое время считался устаревшим. Пользователям рекомендуется использовать модуль <threading> . Поэтому в Python 3 модуль «поток» больше не доступен. Он был переименован в « <_thread> » для обратной несовместимости в Python3.

Чтобы создать новый поток с помощью модуля <_thread> , нам нужно вызвать его метод start_new_thread . Работу этого метода можно понять с помощью следующего синтаксиса —

_thread.start_new_thread ( function, args[, kwargs] )

Здесь —

  • args это кортеж аргументов

  • kwargs — необязательный словарь аргументов ключевых слов

args это кортеж аргументов

kwargs — необязательный словарь аргументов ключевых слов

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

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

пример

Ниже приведен пример создания нового потока с использованием модуля <_thread> . Мы используем метод start_new_thread () здесь.

import _thread
import time

def print_time( threadName, delay):
   count = 0
   while count < 5:
      time.sleep(delay)
      count += 1
      print ("%s: %s" % ( threadName, time.ctime(time.time()) ))

try:
   _thread.start_new_thread( print_time, ("Thread-1", 2, ) )
   _thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
   print ("Error: unable to start thread")
while 1:
   pass

Выход

Следующий вывод поможет нам понять создание новых потоков с помощью модуля <_thread> .

Thread-1: Mon Apr 23 10:03:33 2018
Thread-2: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:35 2018
Thread-1: Mon Apr 23 10:03:37 2018
Thread-2: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:39 2018
Thread-1: Mon Apr 23 10:03:41 2018
Thread-2: Mon Apr 23 10:03:43 2018
Thread-2: Mon Apr 23 10:03:47 2018
Thread-2: Mon Apr 23 10:03:51 2018

модуль <threading>

Модуль <threading> реализуется объектно-ориентированным способом и обрабатывает каждый поток как объект. Следовательно, он обеспечивает гораздо более мощную высокоуровневую поддержку потоков, чем модуль <_thread>. Этот модуль включен в Python 2.4.

Дополнительные методы в модуле <threading>

Модуль <threading> включает в себя все методы модуля <_thread>, но также предоставляет дополнительные методы. Дополнительные методы заключаются в следующем —

  • threading.activeCount () — этот метод возвращает количество активных объектов потока

  • threading.currentThread () — Этот метод возвращает количество объектов потока в элементе управления потоком вызывающей стороны.

  • threading.enumerate () — этот метод возвращает список всех объектов потока, которые в данный момент активны.

  • Для реализации многопоточности модуль <threading> имеет класс Thread, который предоставляет следующие методы:

    • run () — Метод run () является точкой входа для потока.

    • start () — метод start () запускает поток, вызывая метод run.

    • join ([время]) — join () ожидает завершения потоков.

    • isAlive () — метод isAlive () проверяет, выполняется ли еще поток.

    • getName () — Метод getName () возвращает имя потока.

    • setName () — Метод setName () устанавливает имя потока.

threading.activeCount () — этот метод возвращает количество активных объектов потока

threading.currentThread () — Этот метод возвращает количество объектов потока в элементе управления потоком вызывающей стороны.

threading.enumerate () — этот метод возвращает список всех объектов потока, которые в данный момент активны.

Для реализации многопоточности модуль <threading> имеет класс Thread, который предоставляет следующие методы:

run () — Метод run () является точкой входа для потока.

start () — метод start () запускает поток, вызывая метод run.

join ([время]) — join () ожидает завершения потоков.

isAlive () — метод isAlive () проверяет, выполняется ли еще поток.

getName () — Метод getName () возвращает имя потока.

setName () — Метод setName () устанавливает имя потока.

Как создавать темы, используя модуль <threading>?

В этом разделе мы узнаем, как создавать потоки с помощью модуля <threading> . Выполните следующие шаги, чтобы создать новую тему, используя модуль <threading> —

  • Шаг 1 — На этом шаге нам нужно определить новый подкласс класса Thread .

  • Шаг 2 — Затем для добавления дополнительных аргументов нам нужно переопределить метод __init __ (self [, args]) .

  • Шаг 3 — На этом шаге нам нужно переопределить метод run (self [, args]), чтобы реализовать то, что поток должен делать при запуске.

  • Теперь, после создания нового подкласса Thread , мы можем создать его экземпляр и затем запустить новый поток, вызвав start () , который, в свою очередь, вызывает метод run () .

Шаг 1 — На этом шаге нам нужно определить новый подкласс класса Thread .

Шаг 2 — Затем для добавления дополнительных аргументов нам нужно переопределить метод __init __ (self [, args]) .

Шаг 3 — На этом шаге нам нужно переопределить метод run (self [, args]), чтобы реализовать то, что поток должен делать при запуске.

Теперь, после создания нового подкласса Thread , мы можем создать его экземпляр и затем запустить новый поток, вызвав start () , который, в свою очередь, вызывает метод run () .

пример

Рассмотрим этот пример, чтобы узнать, как создать новый поток с помощью модуля <threading> .

import threading
import time
exitFlag = 0

class myThread (threading.Thread):
   def __init__(self, threadID, name, counter):
      threading.Thread.__init__(self)
      self.threadID = threadID
      self.name = name
      self.counter = counter
   def run(self):
      print ("Starting " + self.name)
      print_time(self.name, self.counter, 5)
      print ("Exiting " + self.name)
def print_time(threadName, delay, counter):
   while counter:
      if exitFlag:
         threadName.exit()
      time.sleep(delay)
      print ("%s: %s" % (threadName, time.ctime(time.time())))
      counter -= 1

thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("Exiting Main Thread")
Starting Thread-1
Starting Thread-2

Выход

Теперь рассмотрим следующий вывод —

Thread-1: Mon Apr 23 10:52:09 2018
Thread-1: Mon Apr 23 10:52:10 2018
Thread-2: Mon Apr 23 10:52:10 2018
Thread-1: Mon Apr 23 10:52:11 2018
Thread-1: Mon Apr 23 10:52:12 2018
Thread-2: Mon Apr 23 10:52:12 2018
Thread-1: Mon Apr 23 10:52:13 2018
Exiting Thread-1
Thread-2: Mon Apr 23 10:52:14 2018
Thread-2: Mon Apr 23 10:52:16 2018
Thread-2: Mon Apr 23 10:52:18 2018
Exiting Thread-2
Exiting Main Thread

Программа Python для различных состояний потоков

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

Следующая программа на Python с помощью методов start (), sleep () и join () покажет, как поток вошел в состояние выполнения, ожидания и ожидания соответственно.

Шаг 1 — Импортируйте необходимые модули, <threading> и <time>

import threading
import time

Шаг 2 — Определите функцию, которая будет вызываться при создании потока.

def thread_states():
   print("Thread entered in running state")

Шаг 3 — Мы используем метод времени sleep () модуля, чтобы заставить наш поток ждать, скажем, 2 секунды.

time.sleep(2)

Шаг 4 — Теперь мы создаем поток с именем T1, который принимает аргумент функции, определенной выше.

T1 = threading.Thread(target=thread_states)

Шаг 5 — Теперь с помощью функции start () мы можем запустить наш поток. Он выдаст сообщение, которое было установлено нами при определении функции.

T1.start()
Thread entered in running state

Шаг 6 — Теперь, наконец, мы можем завершить поток с помощью метода join () после того, как он завершит свое выполнение.

T1.join()

Начиная поток в Python

В python мы можем запустить новый поток различными способами, но самый простой из них — определить его как одну функцию. После определения функции мы можем передать это как цель для нового объекта Threading.Thread и так далее. Выполните следующий код Python, чтобы понять, как работает функция —

import threading
import time
import random
def Thread_execution(i):
   print("Execution of Thread {} started\n".format(i))
   sleepTime = random.randint(1,4)
   time.sleep(sleepTime)
   print("Execution of Thread {} finished".format(i))
for i in range(4):
   thread = threading.Thread(target=Thread_execution, args=(i,))
   thread.start()
   print("Active Threads:" , threading.enumerate())

Выход

Execution of Thread 0 started
Active Threads:
   [<_MainThread(MainThread, started 6040)>,
      <HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
      <Thread(Thread-3576, started 3932)>]

Execution of Thread 1 started
Active Threads:
   [<_MainThread(MainThread, started 6040)>,
      <HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
      <Thread(Thread-3576, started 3932)>,
      <Thread(Thread-3577, started 3080)>]

Execution of Thread 2 started
Active Threads:
   [<_MainThread(MainThread, started 6040)>,
      <HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
      <Thread(Thread-3576, started 3932)>,
      <Thread(Thread-3577, started 3080)>,
      <Thread(Thread-3578, started 2268)>]

Execution of Thread 3 started
Active Threads:
   [<_MainThread(MainThread, started 6040)>,
      <HistorySavingThread(IPythonHistorySavingThread, started 5968)>,
      <Thread(Thread-3576, started 3932)>,
      <Thread(Thread-3577, started 3080)>,
      <Thread(Thread-3578, started 2268)>,
      <Thread(Thread-3579, started 4520)>]
Execution of Thread 0 finished
Execution of Thread 1 finished
Execution of Thread 2 finished
Execution of Thread 3 finished

Демонические потоки в Python

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

import threading
import time

def nondaemonThread():
   print("starting my thread")
   time.sleep(8)
   print("ending my thread")
def daemonThread():
   while True:
   print("Hello")
   time.sleep(2)
if __name__ == '__main__':
   nondaemonThread = threading.Thread(target = nondaemonThread)
   daemonThread = threading.Thread(target = daemonThread)
   daemonThread.setDaemon(True)
   daemonThread.start()
   nondaemonThread.start()

В приведенном выше коде есть две функции, а именно: > nondaemonThread () и > daemonThread () . Первая функция печатает свое состояние и спит через 8 секунд, в то время как функция deamonThread () печатает Hello через каждые 2 секунды бесконечно. Мы можем понять разницу между не-демонами и потоками демонов с помощью следующего вывода:

Hello

starting my thread
Hello
Hello
Hello
Hello
ending my thread
Hello
Hello
Hello
Hello
Hello

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

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

синхронизирующий

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

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

Мы можем столкнуться с проблемами при реализации параллельного программирования или применении синхронизирующих примитивов. В этом разделе мы обсудим два основных вопроса. Проблемы —

  • тупик
  • Состояние гонки

Состояние гонки

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

пример

Рассмотрим этот пример, чтобы понять концепцию состояния гонки —

Шаг 1 — На этом шаге нам нужно импортировать модуль потоков —

import threading

Шаг 2 — Теперь определите глобальную переменную, скажем, х, а ее значение равно 0 —

x = 0

Шаг 3 — Теперь нам нужно определить функцию increment_global () , которая будет делать приращение на 1 в этой глобальной функции x —

def increment_global():

   global x
   x += 1

Шаг 4 — На этом шаге мы определим функцию taskofThread () , которая будет вызывать функцию increment_global () указанное количество раз; для нашего примера это 50000 раз —

def taskofThread():

   for _ in range(50000):
      increment_global()

Шаг 5 — Теперь определите функцию main (), в которой создаются потоки t1 и t2. Оба будут запускаться с помощью функции start () и ждать, пока они закончат свою работу с помощью функции join ().

def main():
   global x
   x = 0
   
   t1 = threading.Thread(target= taskofThread)
   t2 = threading.Thread(target= taskofThread)

   t1.start()
   t2.start()

   t1.join()
   t2.join()

Шаг 6 — Теперь нам нужно указать диапазон для количества итераций, которые мы хотим вызвать функцией main (). Здесь мы называем это 5 раз.

if __name__ == "__main__":
   for i in range(5):
      main()
      print("x = {1} after Iteration {0}".format(i,x))

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

Выход

x = 100000 after Iteration 0
x = 54034 after Iteration 1
x = 80230 after Iteration 2
x = 93602 after Iteration 3
x = 93289 after Iteration 4

Работа с состоянием гонки с использованием замков

Поскольку мы видели эффект состояния гонки в приведенной выше программе, нам нужен инструмент синхронизации, который может работать с состоянием гонки между несколькими потоками. В Python модуль <threading> предоставляет класс Lock для работы с условиями гонки. Кроме того, класс Lock предоставляет различные методы, с помощью которых мы можем обрабатывать состояние гонки между несколькими потоками. Методы описаны ниже —

приобретать () метод

Этот метод используется для получения, т. Е. Блокировки блокировки. Блокировка может быть блокирующей или неблокирующей в зависимости от следующего истинного или ложного значения —

  • Со значением, установленным в True — если метод acqu () вызывается с True, который является аргументом по умолчанию, то выполнение потока блокируется до тех пор, пока блокировка не будет разблокирована.

  • При значении False — если метод acqu () вызывается с False, который не является аргументом по умолчанию, то выполнение потока не блокируется, пока не будет установлено значение true, т. Е. До тех пор, пока он не будет заблокирован.

Со значением, установленным в True — если метод acqu () вызывается с True, который является аргументом по умолчанию, то выполнение потока блокируется до тех пор, пока блокировка не будет разблокирована.

При значении False — если метод acqu () вызывается с False, который не является аргументом по умолчанию, то выполнение потока не блокируется, пока не будет установлено значение true, т. Е. До тех пор, пока он не будет заблокирован.

метод release ()

Этот метод используется для снятия блокировки. Ниже приведены несколько важных задач, связанных с этим методом —

  • Если блокировка заблокирована, то метод release () разблокирует ее. Его работа заключается в том, чтобы позволить ровно одному потоку продолжаться, если более одного потока заблокированы и ожидают разблокировки блокировки.

  • Он вызовет ThreadError, если блокировка уже разблокирована.

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

Он вызовет ThreadError, если блокировка уже разблокирована.

Теперь мы можем переписать вышеупомянутую программу с помощью класса блокировки и его методов, чтобы избежать условия гонки. Нам нужно определить метод taskofThread () с аргументом блокировки, а затем использовать методы acqu () и release () для блокировки и неблокирования блокировок, чтобы избежать состояния гонки.

пример

Ниже приведен пример программы на python для понимания концепции блокировок для работы с состоянием гонки.

import threading

x = 0

def increment_global():

   global x
   x += 1

def taskofThread(lock):

   for _ in range(50000):
      lock.acquire()
      increment_global()
      lock.release()

def main():
   global x
   x = 0

   lock = threading.Lock()
   t1 = threading.Thread(target = taskofThread, args = (lock,))
   t2 = threading.Thread(target = taskofThread, args = (lock,))

   t1.start()
   t2.start()

   t1.join()
   t2.join()

if __name__ == "__main__":
   for i in range(5):
      main()
      print("x = {1} after Iteration {0}".format(i,x))

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

Выход

x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4

Тупики — проблема Обеденных Философов

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

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

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

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

Тупик в параллельной системе

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

Решение с помощью программы Python

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

пример

Следующая программа на Python поможет нам найти решение проблемы столового философа —

import threading
import random
import time

class DiningPhilosopher(threading.Thread):

   running = True

   def __init__(self, xname, Leftfork, Rightfork):
   threading.Thread.__init__(self)
   self.name = xname
   self.Leftfork = Leftfork
   self.Rightfork = Rightfork

   def run(self):
   while(self.running):
      time.sleep( random.uniform(3,13))
      print ('%s is hungry.' % self.name)
      self.dine()

   def dine(self):
   fork1, fork2 = self.Leftfork, self.Rightfork

   while self.running:
      fork1.acquire(True)
      locked = fork2.acquire(False)
	  if locked: break
      fork1.release()
      print ('%s swaps forks' % self.name)
      fork1, fork2 = fork2, fork1
   else:
      return

   self.dining()
   fork2.release()
   fork1.release()

   def dining(self):
   print ('%s starts eating '% self.name)
   time.sleep(random.uniform(1,10))
   print ('%s finishes eating and now thinking.' % self.name)

def Dining_Philosophers():
   forks = [threading.Lock() for n in range(5)]
   philosopherNames = ('1st','2nd','3rd','4th', '5th')

   philosophers= [DiningPhilosopher(philosopherNames[i], forks[i%5], forks[(i+1)%5]) \
      for i in range(5)]

   random.seed()
   DiningPhilosopher.running = True
   for p in philosophers: p.start()
   time.sleep(30)
   DiningPhilosopher.running = False
   print (" It is finishing.")

Dining_Philosophers()

Вышеприведенная программа использует концепцию жадных и щедрых философов. Программа также использовала методы acqu () и release () класса Lock модуля <threading> . Мы можем увидеть решение в следующем выводе —

Выход

4th is hungry.
4th starts eating
1st is hungry.
1st starts eating
2nd is hungry.
5th is hungry.
3rd is hungry.
1st finishes eating and now thinking.3rd swaps forks
2nd starts eating
4th finishes eating and now thinking.
3rd swaps forks5th starts eating
5th finishes eating and now thinking.
4th is hungry.
4th starts eating
2nd finishes eating and now thinking.
3rd swaps forks
1st is hungry.
1st starts eating
4th finishes eating and now thinking.
3rd starts eating
5th is hungry.
5th swaps forks
1st finishes eating and now thinking.
5th starts eating
2nd is hungry.
2nd swaps forks
4th is hungry.
5th finishes eating and now thinking.
3rd finishes eating and now thinking.
2nd starts eating 4th starts eating
It is finishing.

Общение Тем

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

Рассмотрим следующие важные моменты, связанные с взаимосвязью потоков:

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

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

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

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

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

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

Структуры данных Python для поточно-ориентированной связи

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

наборы

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

пример

Вот пример расширения класса в Python —

class extend_class(set):
   def __init__(self, *args, **kwargs):
      self._lock = Lock()
      super(extend_class, self).__init__(*args, **kwargs)

   def add(self, elem):
      self._lock.acquire()
	  try:
      super(extend_class, self).add(elem)
      finally:
      self._lock.release()
  
   def delete(self, elem):
      self._lock.acquire()
      try:
      super(extend_class, self).delete(elem)
      finally:
      self._lock.release()

В приведенном выше примере был определен объект класса extended_class , который унаследован от класса набора Python. Объект блокировки создается в конструкторе этого класса. Теперь есть две функции — add () и delete () . Эти функции определены и являются потокобезопасными. Они оба полагаются на функциональность суперкласса с одним ключевым исключением.

декоратор

Это еще один ключевой метод для потоковой связи — использование декораторов.

пример

Рассмотрим пример Python, который показывает, как использовать декораторы & mminus;

def lock_decorator(method):

   def new_deco_method(self, *args, **kwargs):
      with self._lock:
         return method(self, *args, **kwargs)
return new_deco_method

class Decorator_class(set):
   def __init__(self, *args, **kwargs):
      self._lock = Lock()
      super(Decorator_class, self).__init__(*args, **kwargs)

   @lock_decorator
   def add(self, *args, **kwargs):
      return super(Decorator_class, self).add(elem)
   @lock_decorator
   def delete(self, *args, **kwargs):
      return super(Decorator_class, self).delete(elem)

В приведенном выше примере был определен метод декоратора с именем lock_decorator, который также наследуется от класса метода Python. Затем в конструкторе этого класса создается объект блокировки. Теперь есть две функции — add () и delete (). Эти функции определены и являются потокобезопасными. Они оба полагаются на функциональность суперкласса с одним ключевым исключением.

Списки

Структура данных списка является поточно-ориентированной, быстрой и простой структурой для временного хранения в памяти. В Cpython GIL защищает от одновременного доступа к ним. Как мы узнали, списки являются поточно-ориентированными, но как насчет данных, лежащих в них. На самом деле, данные списка не защищены. Например, L.append (x) не гарантирует возврата ожидаемого результата, если другой поток пытается сделать то же самое. Это потому, что, хотя append () является атомарной операцией и поточно-ориентированной, но другой поток пытается изменить данные списка одновременно, поэтому мы можем видеть побочные эффекты условий гонки на выходе.

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

Некоторые другие атомарные операции над списками следующие:

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

Здесь —

  • L, L1, L2 все списки
  • D, D1, D2 — это диктанты
  • х, у объекты
  • я, j являются целыми

Очереди

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

Очереди

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

Типы очередей

В этом разделе мы заработаем о различных типах очередей. Python предоставляет три варианта очередей для использования из модуля <queue>

  • Нормальные очереди (FIFO, первый на первом)
  • LIFO, последний на первом
  • приоритет

Мы узнаем о различных очередях в следующих разделах.

Нормальные очереди (FIFO, первый на первом)

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

ФИФО

Реализация Python FIFO Queue

В Python очередь FIFO может быть реализована как с одним потоком, так и с многопоточностью.

FIFO очередь с одним потоком

Для реализации очереди FIFO с одним потоком в классе Queue будет реализован базовый контейнер типа «первым пришел — первым вышел». Элементы будут добавлены к одному «концу» последовательности, используя put () , и удалены с другого конца, используя get () .

пример

Ниже приведена программа на Python для реализации очереди FIFO с одним потоком:

import queue

q = queue.Queue()

for i in range(8):
   q.put("item-" + str(i))

while not q.empty():
   print (q.get(), end = " ")

Выход

item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7

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

FIFO очередь с несколькими потоками

Для реализации FIFO с несколькими потоками нам нужно определить функцию myqueue (), которая расширена из модуля очереди. Работа методов get () и put () аналогична описанной выше при реализации очереди FIFO с одним потоком. Затем, чтобы сделать его многопоточным, нам нужно объявить и создать экземпляры потоков. Эти потоки будут использовать очередь в режиме FIFO.

пример

Ниже приведена программа на Python для реализации очереди FIFO с несколькими потоками.

import threading
import queue
import random
import time
def myqueue(queue):
   while not queue.empty():
   item = queue.get()
   if item is None:
   break
   print("{} removed {} from the queue".format(threading.current_thread(), item))
   queue.task_done()
   time.sleep(2)
q = queue.Queue()
for i in range(5):
   q.put(i)
threads = []
for i in range(4):
   thread = threading.Thread(target=myqueue, args=(q,))
   thread.start()
   threads.append(thread)
for thread in threads:
   thread.join()

Выход

<Thread(Thread-3654, started 5044)> removed 0 from the queue
<Thread(Thread-3655, started 3144)> removed 1 from the queue
<Thread(Thread-3656, started 6996)> removed 2 from the queue
<Thread(Thread-3657, started 2672)> removed 3 from the queue
<Thread(Thread-3654, started 5044)> removed 4 from the queue

LIFO, очередь «Последний пришел первым»

Эта очередь использует полностью противоположную аналогию с очередями FIFO (первым пришел — первым вышел). В этом механизме организации очереди тот, кто придет последним, получит службу первым. Это похоже на реализацию структуры данных стека. Очереди LIFO оказываются полезными при реализации поиска в глубину как алгоритмы искусственного интеллекта.

Python-реализация очереди LIFO

В Python очередь LIFO может быть реализована как с одним потоком, так и с многопоточностью.

LIFO очередь с одним потоком

Для реализации очереди LIFO с одним потоком класс Queue реализует базовый контейнер типа «последний пришел — первый вышел», используя структуру Queue .LifoQueue. Теперь при вызове метода put () элементы добавляются в начало контейнера и удаляются из него также при использовании get () .

пример

Ниже приведена программа на Python для реализации очереди LIFO с одним потоком:

import queue

q = queue.LifoQueue()

for i in range(8):
   q.put("item-" + str(i))

while not q.empty():
   print (q.get(), end=" ")
Output:
item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0

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

Очередь LIFO с несколькими потоками

Реализация схожа с реализацией очередей FIFO с несколькими потоками. Единственное отличие состоит в том, что нам нужно использовать класс Queue, который будет реализовывать базовый контейнер « первым пришел — первым вышел», используя структуру Queue.LifoQueue .

пример

Ниже приведена программа на Python для реализации очереди LIFO с несколькими потоками:

import threading
import queue
import random
import time
def myqueue(queue):
   while not queue.empty():
      item = queue.get()
      if item is None:
      break
	  print("{} removed {} from the queue".format(threading.current_thread(), item))
      queue.task_done()
      time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
   q.put(i)
threads = []
for i in range(4):
   thread = threading.Thread(target=myqueue, args=(q,))
   thread.start()
   threads.append(thread)
for thread in threads:
   thread.join() 

Выход

<Thread(Thread-3882, started 4928)> removed 4 from the queue
<Thread(Thread-3883, started 4364)> removed 3 from the queue
<Thread(Thread-3884, started 6908)> removed 2 from the queue
<Thread(Thread-3885, started 3584)> removed 1 from the queue
<Thread(Thread-3882, started 4928)> removed 0 from the queue

Приоритетная очередь

В очередях FIFO и LIFO порядок элементов связан с порядком вставки. Однако во многих случаях приоритет важнее, чем порядок вставки. Давайте рассмотрим пример из реального мира. Предположим, что служба безопасности в аэропорту проверяет людей разных категорий. Люди из VVIP, сотрудники авиакомпании, таможенники, категории могут быть проверены по приоритету, а не проверены по прибытии, как это делается для простых людей.

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

Реализация приоритетов в Python

В Python очередь с приоритетами может быть реализована как с одним потоком, так и с многопоточностью.

Приоритетная очередь с одним потоком

Для реализации приоритетной очереди с одним потоком класс Queue будет реализовывать задачу в приоритетном контейнере с использованием структуры Queue .PriorityQueue. Теперь, при вызове put () , элементы добавляются со значением, где наименьшее значение будет иметь самый высокий приоритет, и, следовательно, сначала извлекается с помощью get () .

пример

Рассмотрим следующую программу на Python для реализации очереди Priority с одним потоком:

import queue as Q
p_queue = Q.PriorityQueue()

p_queue.put((2, 'Urgent'))
p_queue.put((1, 'Most Urgent'))
p_queue.put((10, 'Nothing important'))
prio_queue.put((5, 'Important'))

while not p_queue.empty():
   item = p_queue.get()
   print('%s - %s' % item)

Выход

1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important

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

Приоритетная очередь с несколькими потоками

Реализация аналогична реализации очередей FIFO и LIFO с несколькими потоками. Единственное отличие состоит в том, что нам нужно использовать класс Queue для инициализации приоритета, используя структуру Queue.PriorityQueue . Другое отличие заключается в том, как будет создаваться очередь. В приведенном ниже примере он будет создан с двумя одинаковыми наборами данных.

пример

Следующая программа Python помогает в реализации приоритетной очереди с несколькими потоками —

import threading
import queue
import random
import time
def myqueue(queue):
   while not queue.empty():
      item = queue.get()
      if item is None:
      break
      print("{} removed {} from the queue".format(threading.current_thread(), item))
      queue.task_done()
      time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
   q.put(i,1)

for i in range(5):
   q.put(i,1)

threads = []
for i in range(2):
   thread = threading.Thread(target=myqueue, args=(q,))
   thread.start()
   threads.append(thread)
for thread in threads:
   thread.join()

Выход

<Thread(Thread-4939, started 2420)> removed 0 from the queue
<Thread(Thread-4940, started 3284)> removed 0 from the queue
<Thread(Thread-4939, started 2420)> removed 1 from the queue
<Thread(Thread-4940, started 3284)> removed 1 from the queue
<Thread(Thread-4939, started 2420)> removed 2 from the queue
<Thread(Thread-4940, started 3284)> removed 2 from the queue
<Thread(Thread-4939, started 2420)> removed 3 from the queue
<Thread(Thread-4940, started 3284)> removed 3 from the queue
<Thread(Thread-4939, started 2420)> removed 4 from the queue
<Thread(Thread-4940, started 3284)> removed 4 from the queue

Тестирование потоковых приложений

В этой главе мы узнаем о тестировании потоковых приложений. Мы также узнаем о важности тестирования.

Зачем тестировать?

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

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

Улучшение качества программного обеспечения

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

Удовлетворенность клиентов

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

Уменьшить влияние новых функций

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

Пользовательский опыт

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

Сокращение расходов

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

Что проверить?

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

Рассмотрим следующие важные моменты, связанные с тем, что тестировать —

  • Нам нужно сосредоточиться на тестировании функциональности кода, а не покрытия кода.

  • Нам нужно сначала протестировать наиболее важные части кода, а затем перейти к менее важным частям кода. Это определенно сэкономит время.

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

Нам нужно сосредоточиться на тестировании функциональности кода, а не покрытия кода.

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

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

Подходы для тестирования параллельных программ

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

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

Систематическое исследование

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

Недвижимость приводом

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

Стратегии тестирования

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

Проактивная

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

реагирующий

Подход, при котором тестирование не начинается до завершения процесса разработки.

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

Синтаксические ошибки

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

Семантические ошибки

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

Модульное тестирование

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

В наших последующих разделах мы узнаем о различных модулях Python для модульного тестирования.

модуль unittest

Самый первый модуль для модульного тестирования — это модуль unittest. Он вдохновлен JUnit и по умолчанию включен в Python3.6. Он поддерживает автоматизацию тестирования, совместное использование кода настройки и завершения для тестов, агрегирование тестов в коллекции и независимость тестов от среды отчетности.

Ниже приведены несколько важных концепций, поддерживаемых модулем unittest.

Текстовое крепление

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

Прецедент

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

  • setUp () — метод подключения для настройки тестового прибора перед его применением. Это вызывается перед вызовом реализованных методов тестирования.

  • tearDown ( — метод ловушки для деконструкции объекта класса после запуска всех тестов в классе.

setUp () — метод подключения для настройки тестового прибора перед его применением. Это вызывается перед вызовом реализованных методов тестирования.

tearDown ( — метод ловушки для деконструкции объекта класса после запуска всех тестов в классе.

Тестирование

Это набор тестовых наборов, тестовых случаев или обоих.

Тест бегун

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

пример

Следующая программа на Python использует модуль unittest для тестирования модуля с именем Fibonacci . Программа помогает в расчете ряда Фибоначчи от числа. В этом примере мы создали класс с именем Fibo_test, чтобы определять тестовые случаи с использованием различных методов. Эти методы унаследованы от unittest.TestCase. Мы используем два метода по умолчанию — setUp () и tearDown (). Мы также определяем метод testfibocal. Название теста должно начинаться с буквенного теста. В последнем блоке unittest.main () предоставляет интерфейс командной строки для сценария тестирования.

import unittest
def fibonacci(n):
   a, b = 0, 1
   for i in range(n):
   a, b = b, a + b
   return a
class Fibo_Test(unittest.TestCase):
   def setUp(self):
   print("This is run before our tests would be executed")
   def tearDown(self):
   print("This is run after the completion of execution of our tests")

   def testfibocal(self):
   self.assertEqual(fib(0), 0)
   self.assertEqual(fib(1), 1)
   self.assertEqual(fib(5), 5)
   self.assertEqual(fib(10), 55)
   self.assertEqual(fib(20), 6765)

if __name__ == "__main__":
   unittest.main()

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

Выход

This runs before our tests would be executed.
This runs after the completion of execution of our tests.
.
----------------------------------------------------------------------
Ran 1 test in 0.006s
OK

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

Рассмотрим следующий блок кода в качестве примера —

def fibonacci(n):
   a, b = 0, 1
   for i in range(n):
   a, b = b, a + b
   return a

Несколько изменений в блоке кода сделаны как показано ниже —

def fibonacci(n):
   a, b = 1, 1
   for i in range(n):
   a, b = b, a + b
   return a

Теперь, после запуска скрипта с измененным кодом, мы получим следующий вывод:

This runs before our tests would be executed.
This runs after the completion of execution of our tests.
F
======================================================================
FAIL: testCalculation (__main__.Fibo_Test)
----------------------------------------------------------------------
Traceback (most recent call last):
File "unitg.py", line 15, in testCalculation
self.assertEqual(fib(0), 0)
AssertionError: 1 != 0
----------------------------------------------------------------------
Ran 1 test in 0.007s

FAILED (failures = 1)

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

Docktest модуль

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

Если в нашем коде все хорошо, то из модуля docktest не будет выходных данных; в противном случае он предоставит вывод.

пример

В следующем примере Python используется модуль docktest для тестирования модуля с именем Fibonacci, который помогает в вычислении ряда Фибоначчи от числа.

import doctest
def fibonacci(n):
   """
   Calculates the Fibonacci number

   >>> fibonacci(0)
   0
   >>> fibonacci(1)
   1
   >>> fibonacci(10)
   55
   >>> fibonacci(20)
   6765
   >>>

   """
   a, b = 1, 1
   for i in range(n):
   a, b = b, a + b
   return a
      if __name__ == "__main__":
   doctest.testmod()

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

(base) D:\ProgramData>python dock_test.py -v
Trying:
   fibonacci(0)
Expecting:
   0
ok
Trying:
   fibonacci(1)
Expecting:
   1
ok
Trying:
   fibonacci(10)
Expecting:
   55
ok
Trying:
   fibonacci(20)
Expecting:
   6765
ok
1 items had no tests:
   __main__
1 items passed all tests:
4 tests in __main__.fibonacci
4 tests in 2 items.
4 passed and 0 failed.
Test passed.

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

Рассмотрим следующий блок кода в качестве примера —

def fibonacci(n):
   a, b = 0, 1
   for i in range(n):
   a, b = b, a + b
   return a

Следующий блок кода помогает с изменениями —

def fibonacci(n):
   a, b = 1, 1
   for i in range(n):
   a, b = b, a + b
   return a

После запуска сценария даже без опции –v с измененным кодом мы получим вывод, как показано ниже.

Выход

(base) D:\ProgramData>python dock_test.py
**********************************************************************
File "unitg.py", line 6, in __main__.fibonacci
Failed example:
   fibonacci(0)
Expected:
   0
Got:
   1
**********************************************************************
File "unitg.py", line 10, in __main__.fibonacci
Failed example:
   fibonacci(10)
Expected:
   55
Got:
   89
**********************************************************************
File "unitg.py", line 12, in __main__.fibonacci
Failed example:
   fibonacci(20)
Expected:
   6765
Got:
   10946
**********************************************************************
1 items had failures:
   3 of 4 in __main__.fibonacci
***Test Failed*** 3 failures.

В приведенном выше выводе видно, что три теста не пройдены.

Отладка потоковых приложений

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

Что такое отладка?

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

Python Debugger

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

  • Это позволяет нам проверять значения переменных во время выполнения.
  • Мы можем пройти по коду и установить точки останова также.

Мы можем работать с pdb следующими двумя способами:

  • Через командную строку; это также называется посмертной отладкой.
  • Путем интерактивного запуска pdb.

Работа с pdb

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

import pdb;
pdb.set_trace()

Рассмотрим следующие команды для работы с pdb через командную строку.

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

Ниже приведена демонстрация команды h (help) отладчика Python:

import pdb

pdb.set_trace()
--Call--
>d:\programdata\lib\site-packages\ipython\core\displayhook.py(247)__call__()
-> def __call__(self, result = None):
(Pdb) h

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:
==========================
exec pdb

пример

Работая с отладчиком Python, мы можем установить точку останова в любом месте скрипта, используя следующие строки:

import pdb;
pdb.set_trace()

После установки точки останова мы можем запустить скрипт в обычном режиме. Скрипт будет выполняться до определенного момента; до тех пор, пока не была установлена ​​линия. Рассмотрим следующий пример, где мы будем запускать скрипт, используя вышеупомянутые строки в разных местах скрипта:

import pdb;
a = "aaa"
pdb.set_trace()
b = "bbb"
c = "ccc"
final = a + b + c
print (final)

Когда приведенный выше скрипт выполняется, он будет выполнять программу до a = «aaa», мы можем проверить это в следующем выводе.

Выход

--Return--
> <ipython-input-7-8a7d1b5cc854>(3)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
*** NameError: name 'b' is not defined
(Pdb) p c
*** NameError: name 'c' is not defined

После использования команды p (print) в pdb этот скрипт печатает только aaa. Это сопровождается ошибкой, потому что мы установили точку останова до a = «aaa».

Точно так же мы можем запустить скрипт, изменив точки останова и увидев разницу в выходных данных —

import pdb
a = "aaa"
b = "bbb"
c = "ccc"
pdb.set_trace()
final = a + b + c
print (final)

Выход

--Return--
> <ipython-input-9-a59ef5caf723>(5)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
*** NameError: name 'final' is not defined
(Pdb) exit

В следующем скрипте мы устанавливаем точку останова в последней строке программы —

import pdb
a = "aaa"
b = "bbb"
c = "ccc"
final = a + b + c
pdb.set_trace()
print (final)

Выход выглядит следующим образом —

--Return--
> <ipython-input-11-8019b029997d>(6)<module>()->None
-> pdb.set_trace()
(Pdb) p a
'aaa'
(Pdb) p b
'bbb'
(Pdb) p c
'ccc'
(Pdb) p final
'aaabbbccc'
(Pdb)

Бенчмаркинг и профилирование

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

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

Что такое Бенчмаркинг?

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

Как работает бенчмаркинг?

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

Модуль Python для бенчмаркинга

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

пример

В следующем скрипте Python мы импортируем модуль timeit , который дополнительно измеряет время, затраченное на выполнение двух функций — functionA и functionB

import timeit
import time
def functionA():
   print("Function A starts the execution:")
   print("Function A completes the execution:")
def functionB():
   print("Function B starts the execution")
   print("Function B completes the execution")
start_time = timeit.default_timer()
functionA()
print(timeit.default_timer() - start_time)
start_time = timeit.default_timer()
functionB()
print(timeit.default_timer() - start_time)

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

Выход

Function A starts the execution:
Function A completes the execution:
0.0014599495514175942
Function B starts the execution
Function B completes the execution
0.0017024724827479076

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

В Python мы можем создать наш собственный таймер, который будет действовать как модуль timeit . Это можно сделать с помощью функции декоратора . Ниже приведен пример пользовательского таймера —

import random
import time

def timer_func(func):

   def function_timer(*args, **kwargs):
   start = time.time()
   value = func(*args, **kwargs)
   end = time.time()
   runtime = end - start
   msg = "{func} took {time} seconds to complete its execution."
      print(msg.format(func = func.__name__,time = runtime))
   return value
   return function_timer

@timer_func
def Myfunction():
   for x in range(5):
   sleep_time = random.choice(range(1,3))
   time.sleep(sleep_time)

if __name__ == '__main__':
   Myfunction()

Приведенный выше скрипт на python помогает импортировать случайные модули времени. Мы создали функцию декоратора timer_func (). В нем есть функция function_timer (). Теперь вложенная функция будет захватывать время перед вызовом переданной функции. Затем он ожидает возврата функции и захватывает время окончания. Таким образом, мы наконец можем заставить скрипт Python печатать время выполнения. Скрипт сгенерирует вывод, как показано ниже.

Выход

Myfunction took 8.000457763671875 seconds to complete its execution.

Что такое профилирование?

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

В последующих разделах мы узнаем о различных модулях Python для профилирования.

cProfile — встроенный модуль

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

пример

def increment_global():

   global x
   x += 1

def taskofThread(lock):

   for _ in range(50000):
   lock.acquire()
   increment_global()
   lock.release()

def main():
   global x
   x = 0

   lock = threading.Lock()

   t1 = threading.Thread(target=taskofThread, args=(lock,))
   t2 = threading.Thread(target= taskofThread, args=(lock,))

   t1.start()
   t2.start()

   t1.join()
   t2.join()

if __name__ == "__main__":
   for i in range(5):
      main()
   print("x = {1} after Iteration {0}".format(i,x))

Приведенный выше код сохраняется в файле thread_increment.py . Теперь выполните код с помощью cProfile в командной строке следующим образом:

(base) D:\ProgramData>python -m cProfile thread_increment.py
x = 100000 after Iteration 0
x = 100000 after Iteration 1
x = 100000 after Iteration 2
x = 100000 after Iteration 3
x = 100000 after Iteration 4
      3577 function calls (3522 primitive calls) in 1.688 seconds

   Ordered by: standard name

   ncalls tottime percall cumtime percall filename:lineno(function)

   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:103(release)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:143(__init__)
   5 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap>:147(__enter__)
      

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

  • ncalls — количество звонков.

  • общее время — это общее время, проведенное в данной функции.

  • percall — относится к коэффициенту totaltime, деленному на ncalls.

  • cumtime — совокупное время, проведенное в этой и всех подфункциях. Это даже точно для рекурсивных функций.

  • percall — это частное cumtime, разделенное на примитивные вызовы.

  • filename: lineno (функция) — в основном предоставляет соответствующие данные каждой функции.

ncalls — количество звонков.

общее время — это общее время, проведенное в данной функции.

percall — относится к коэффициенту totaltime, деленному на ncalls.

cumtime — совокупное время, проведенное в этой и всех подфункциях. Это даже точно для рекурсивных функций.

percall — это частное cumtime, разделенное на примитивные вызовы.

filename: lineno (функция) — в основном предоставляет соответствующие данные каждой функции.

Параллелизм в Python — пул потоков

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

  • Если поток в пуле потоков завершает свое выполнение, то этот поток можно использовать повторно.

  • Если поток завершается, будет создан другой поток для его замены.

Если поток в пуле потоков завершает свое выполнение, то этот поток можно использовать повторно.

Если поток завершается, будет создан другой поток для его замены.

Модуль Python — Concurrent.futures

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

В наших последующих разделах мы узнаем о различных классах модуля concurrent.futures.

Executor Class

Executor — это абстрактный класс Python-модуля concurrent.futures . Его нельзя использовать напрямую, и нам нужно использовать один из следующих конкретных подклассов:

  • ThreadPoolExecutor
  • ProcessPoolExecutor

ThreadPoolExecutor — конкретный подкласс

Это один из конкретных подклассов класса Executor. Подкласс использует многопоточность, и мы получаем пул потоков для отправки задач. Этот пул назначает задачи доступным потокам и планирует их запуск.

Как создать ThreadPoolExecutor?

С помощью модуля concurrent.futures и его конкретного подкласса Executor мы можем легко создать пул потоков. Для этого нам нужно создать ThreadPoolExecutor с тем количеством потоков, которое мы хотим в пуле. По умолчанию это число 5. Затем мы можем отправить задачу в пул потоков. Когда мы отправляем () задачу, мы возвращаемся в будущее . У объекта Future есть метод done () , который сообщает, разрешено ли будущее. При этом значение было установлено для этого конкретного будущего объекта. Когда задача завершается, исполнитель пула потоков устанавливает значение для будущего объекта.

пример

from concurrent.futures import ThreadPoolExecutor
from time import sleep
def task(message):
   sleep(2)
   return message

def main():
   executor = ThreadPoolExecutor(5)
   future = executor.submit(task, ("Completed"))
   print(future.done())
   sleep(2)
   print(future.done())
   print(future.result())
if __name__ == '__main__':
main()

Выход

False
True
Completed

В приведенном выше примере ThreadPoolExecutor был создан с 5 потоками. Затем задача, которая будет ожидать сообщения в течение 2 секунд, передается исполнителю пула потоков. Как видно из выходных данных, задача не завершается до 2 секунд, поэтому первый вызов done () вернет False. Через 2 секунды задача будет выполнена, и мы получим результат будущего, вызвав для него метод result () .

Создание ThreadPoolExecutor — Менеджер контекста

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

with ThreadPoolExecutor(max_workers = 5) as executor

пример

Следующий пример заимствован из документации по Python. В этом примере, прежде всего, необходимо импортировать модуль concurrent.futures . Затем создается функция с именем load_url (), которая загружает запрошенный URL. Затем функция создает ThreadPoolExecutor с 5 потоками в пуле. ThreadPoolExecutor был использован как менеджер контекста. Мы можем получить результат будущего, вызвав для него метод result () .

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/',
   'http://www.cnn.com/',
   'http://europe.wsj.com/',
   'http://www.bbc.co.uk/',
   'http://some-made-up-domain.com/']

def load_url(url, timeout):
   with urllib.request.urlopen(url, timeout = timeout) as conn:
   return conn.read()

with concurrent.futures.ThreadPoolExecutor(max_workers = 5) as executor:

   future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
   for future in concurrent.futures.as_completed(future_to_url):
   url = future_to_url[future]
   try:
      data = future.result()
   except Exception as exc:
      print('%r generated an exception: %s' % (url, exc))
   else:
      print('%r page is %d bytes' % (url, len(data)))

Выход

Ниже приведен вывод приведенного выше скрипта Python —

'http://some-made-up-domain.com/' generated an exception: <urlopen error [Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229313 bytes
'http://www.cnn.com/' page is 168933 bytes
'http://www.bbc.co.uk/' page is 283893 bytes
'http://europe.wsj.com/' page is 938109 bytes

Использование функции Executor.map ()

Функция Python map () широко используется в ряде задач. Одна из таких задач — применить определенную функцию к каждому элементу итерируемых элементов. Точно так же мы можем отобразить все элементы итератора в функцию и передать их как независимые задания в ThreadPoolExecutor . Рассмотрим следующий пример скрипта Python, чтобы понять, как работает функция.

пример

В этом примере ниже функция map используется для применения функции square () к каждому значению в массиве значений.

from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
   return n * n
def main():
   with ThreadPoolExecutor(max_workers = 3) as executor:
      results = executor.map(square, values)
for result in results:
      print(result)
if __name__ == '__main__':
   main()

Выход

Приведенный выше скрипт Python генерирует следующий вывод:

4
9
16
25

Параллелизм в Python — пул процессов

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

Модуль Python — Concurrent.futures

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

В наших последующих разделах мы рассмотрим различные подклассы модуля concurrent.futures.

Executor Class

Executor — это абстрактный класс Python-модуля concurrent.futures . Его нельзя использовать напрямую, и нам нужно использовать один из следующих конкретных подклассов:

  • ThreadPoolExecutor
  • ProcessPoolExecutor

ProcessPoolExecutor — конкретный подкласс

Это один из конкретных подклассов класса Executor. Он использует мульти-обработку, и мы получаем пул процессов для отправки задач. Этот пул назначает задачи доступным процессам и планирует их запуск.

Как создать ProcessPoolExecutor?

С помощью модуля concurrent.futures и его конкретного подкласса Executor мы можем легко создать пул процессов. Для этого нам нужно создать ProcessPoolExecutor с количеством процессов, которые мы хотим в пуле. По умолчанию это число 5. Затем следует отправить задачу в пул процессов.

пример

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

from concurrent.futures import ProcessPoolExecutor
from time import sleep
def task(message):
   sleep(2)
   return message

def main():
   executor = ProcessPoolExecutor(5)
   future = executor.submit(task, ("Completed"))
   print(future.done())
   sleep(2)
   print(future.done())
   print(future.result())
if __name__ == '__main__':
main()

Выход

False
False
Completed

В приведенном выше примере Process PoolExecutor был создан с 5 потоками. Затем задача, которая будет ожидать сообщения в течение 2 секунд, передается исполнителю пула процессов. Как видно из выходных данных, задача не завершается до 2 секунд, поэтому первый вызов done () вернет False. Через 2 секунды задача будет выполнена, и мы получим результат будущего, вызвав для него метод result () .

Создание ProcessPoolExecutor — Менеджер контекста

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

with ProcessPoolExecutor(max_workers = 5) as executor

пример

Для лучшего понимания мы берем тот же пример, который использовался при создании пула потоков. В этом примере нам нужно начать с импорта модуля concurrent.futures . Затем создается функция с именем load_url (), которая загружает запрошенный URL. Затем создается ProcessPoolExecutor с 5 числами потоков в пуле. Process PoolExecutor был использован как менеджер контекста. Мы можем получить результат будущего, вызвав для него метод result () .

import concurrent.futures
from concurrent.futures import ProcessPoolExecutor
import urllib.request

URLS = ['http://www.foxnews.com/',
   'http://www.cnn.com/',
   'http://europe.wsj.com/',
   'http://www.bbc.co.uk/',
   'http://some-made-up-domain.com/']

def load_url(url, timeout):
   with urllib.request.urlopen(url, timeout = timeout) as conn:
      return conn.read()

def main():
   with concurrent.futures.ProcessPoolExecutor(max_workers=5) as executor:
      future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
      for future in concurrent.futures.as_completed(future_to_url):
      url = future_to_url[future]
      try:
         data = future.result()
      except Exception as exc:
         print('%r generated an exception: %s' % (url, exc))
      else:
         print('%r page is %d bytes' % (url, len(data)))

if __name__ == '__main__':
   main()

Выход

Приведенный выше скрипт Python сгенерирует следующий вывод:

'http://some-made-up-domain.com/' generated an exception: <urlopen error [Errno 11004] getaddrinfo failed>
'http://www.foxnews.com/' page is 229476 bytes
'http://www.cnn.com/' page is 165323 bytes
'http://www.bbc.co.uk/' page is 284981 bytes
'http://europe.wsj.com/' page is 967575 bytes

Использование функции Executor.map ()

Функция Python map () широко используется для выполнения ряда задач. Одна из таких задач — применить определенную функцию к каждому элементу итерируемых элементов. Точно так же мы можем отобразить все элементы итератора в функцию и отправить их как независимые задания в ProcessPoolExecutor . Рассмотрим следующий пример скрипта Python, чтобы понять это.

пример

Мы рассмотрим тот же пример, который мы использовали при создании пула потоков с помощью функции Executor.map () . В приведенном ниже примере функция map используется для применения функции square () к каждому значению в массиве значений.

from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import as_completed
values = [2,3,4,5]
def square(n):
   return n * n
def main():
   with ProcessPoolExecutor(max_workers = 3) as executor:
      results = executor.map(square, values)
   for result in results:
      print(result)
if __name__ == '__main__':
   main()

Выход

Приведенный выше скрипт Python сгенерирует следующий вывод

4
9
16
25

Когда использовать ProcessPoolExecutor и ThreadPoolExecutor?

Теперь, когда мы изучили оба класса Executor — ThreadPoolExecutor и ProcessPoolExecutor, нам нужно знать, когда использовать какого исполнителя. Нам нужно выбрать ProcessPoolExecutor в случае связанных с CPU рабочих нагрузок и ThreadPoolExecutor в случае связанных с I / O рабочих нагрузок.

Если мы используем ProcessPoolExecutor , то нам не нужно беспокоиться о GIL, поскольку он использует многопроцессорность. Более того, время выполнения будет меньше по сравнению с ThreadPoolExecution . Рассмотрим следующий пример скрипта Python, чтобы понять это.

пример

import time
import concurrent.futures

value = [8000000, 7000000]

def counting(n):
   start = time.time()
   while n > 0:
      n -= 1
   return time.time() - start

def main():
   start = time.time()
   with concurrent.futures.ProcessPoolExecutor() as executor:
      for number, time_taken in zip(value, executor.map(counting, value)):
         print('Start: {} Time taken: {}'.format(number, time_taken))
   print('Total time taken: {}'.format(time.time() - start))

if __name__ == '__main__':
main()

Выход

Start: 8000000 Time taken: 1.5509998798370361
Start: 7000000 Time taken: 1.3259999752044678
Total time taken: 2.0840001106262207

Example- Python script with ThreadPoolExecutor:
import time
import concurrent.futures

value = [8000000, 7000000]

def counting(n):
   start = time.time()
   while n > 0:
      n -= 1
   return time.time() - start

def main():
   start = time.time()
   with concurrent.futures.ThreadPoolExecutor() as executor:
      for number, time_taken in zip(value, executor.map(counting, value)):
         print('Start: {} Time taken: {}'.format(number, time_taken))
      print('Total time taken: {}'.format(time.time() - start))

if __name__ == '__main__':
main()

Выход

Start: 8000000 Time taken: 3.8420000076293945
Start: 7000000 Time taken: 3.6010000705718994
Total time taken: 3.8480000495910645

Из выводов обеих программ, приведенных выше, мы видим разницу во времени выполнения при использовании ProcessPoolExecutor и ThreadPoolExecutor .

Параллелизм в Python — многопроцессорность

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

многопроцессорная обработка

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

Многопоточность

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

Следующая таблица показывает некоторые важные различия между ними —

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

Устранение влияния глобальной блокировки интерпретатора (GIL)

При работе с параллельными приложениями в Python есть ограничение, называемое GIL (Global Interpreter Lock) . GIL никогда не позволяет нам использовать несколько ядер CPU, и поэтому мы можем сказать, что в Python нет настоящих потоков. GIL — мьютекс — блокировка взаимного исключения, которая делает вещи безопасными. Другими словами, мы можем сказать, что GIL препятствует параллельному выполнению кода Python несколькими потоками. Блокировка может удерживаться только одним потоком за раз, и если мы хотим выполнить поток, он должен сначала получить блокировку.

Используя многопроцессорность, мы можем эффективно обойти ограничение, вызванное GIL —

  • Используя многопроцессорность, мы используем возможности нескольких процессов и, следовательно, мы используем несколько экземпляров GIL.

  • В связи с этим нет ограничений на выполнение байт-кода одного потока в наших программах одновременно.

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

В связи с этим нет ограничений на выполнение байт-кода одного потока в наших программах одновременно.

Запуск процессов в Python

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

  • вилка
  • Порождать
  • Forkserver

Создание процесса с помощью Fork

Команда Fork — это стандартная команда в UNIX. Он используется для создания новых процессов, называемых дочерними процессами. Этот дочерний процесс выполняется одновременно с процессом, называемым родительским процессом. Эти дочерние процессы также идентичны своим родительским процессам и наследуют все ресурсы, доступные родительскому процессу. Следующие системные вызовы используются при создании процесса с помощью Fork —

  • fork () — это системный вызов, обычно реализованный в ядре. Он используется для создания копии процесса.

  • getpid () — этот системный вызов возвращает идентификатор процесса (PID) вызывающего процесса.

fork () — это системный вызов, обычно реализованный в ядре. Он используется для создания копии процесса.

getpid () — этот системный вызов возвращает идентификатор процесса (PID) вызывающего процесса.

пример

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

import os

def child():
   n = os.fork()
   
   if n > 0:
      print("PID of Parent process is : ", os.getpid())

   else:
      print("PID of Child process is : ", os.getpid())
child()

Выход

PID of Parent process is : 25989
PID of Child process is : 25990

Создание процесса с помощью Spawn

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

  • Импорт многопроцессорного модуля.

  • Создание объекта процесса.

  • Запуск процесса с помощью вызова метода start () .

  • Дождитесь, пока процесс завершит свою работу, и выйдите, вызвав метод join () .

Импорт многопроцессорного модуля.

Создание объекта процесса.

Запуск процесса с помощью вызова метода start () .

Дождитесь, пока процесс завершит свою работу, и выйдите, вызвав метод join () .

пример

Следующий пример скрипта Python помогает в порождении трех процессов

import multiprocessing

def spawn_process(i):
   print ('This is process: %s' %i)
   return

if __name__ == '__main__':
   Process_jobs = []
   for i in range(3):
   p = multiprocessing.Process(target = spawn_process, args = (i,))
      Process_jobs.append(p)
   p.start()
   p.join()

Выход

This is process: 0
This is process: 1
This is process: 2

Создание процесса с помощью Forkserver

Механизм Forkserver доступен только на тех выбранных платформах UNIX, которые поддерживают передачу файловых дескрипторов по каналам Unix. Рассмотрим следующие моменты, чтобы понять работу механизма Forkserver —

  • Сервер создается с использованием механизма Forkserver для запуска нового процесса.

  • Затем сервер получает команду и обрабатывает все запросы на создание новых процессов.

  • Для создания нового процесса наша программа на Python отправит запрос в Forkserver и создаст для нас процесс.

  • Наконец, мы можем использовать этот новый созданный процесс в наших программах.

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

Затем сервер получает команду и обрабатывает все запросы на создание новых процессов.

Для создания нового процесса наша программа на Python отправит запрос в Forkserver и создаст для нас процесс.

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

Демонстрационные процессы в Python

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

пример

Здесь мы используем тот же пример, что и в потоках демона. Единственное отличие — это изменение модуля с многопоточности на многопроцессорность и установка флага демона в значение true. Тем не менее, будет изменение в выходе, как показано ниже —

import multiprocessing
import time

def nondaemonProcess():
   print("starting my Process")
   time.sleep(8)
   print("ending my Process")
def daemonProcess():
   while True:
   print("Hello")
   time.sleep(2)
if __name__ == '__main__':
   nondaemonProcess = multiprocessing.Process(target = nondaemonProcess)
   daemonProcess = multiprocessing.Process(target = daemonProcess)
   daemonProcess.daemon = True
   nondaemonProcess.daemon = False
   daemonProcess.start()
   nondaemonProcess.start()

Выход

starting my Process
ending my Process

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

Завершение процессов в Python

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

пример

import multiprocessing
import time
def Child_process():
   print ('Starting function')
   time.sleep(5)
   print ('Finished function')
P = multiprocessing.Process(target = Child_process)
P.start()
print("My Process has terminated, terminating main thread")
print("Terminating Child Process")
P.terminate()
print("Child Process successfully terminated")

Выход

My Process has terminated, terminating main thread
Terminating Child Process
Child Process successfully terminated

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

Определение текущего процесса в Python

Каждый процесс в операционной системе имеет идентификатор процесса, известный как PID. В Python мы можем узнать PID текущего процесса с помощью следующей команды —

import multiprocessing
print(multiprocessing.current_process().pid)

пример

Следующий пример скрипта Python помогает узнать PID основного процесса, а также PID дочернего процесса —

import multiprocessing
import time
def Child_process():
   print("PID of Child Process is: {}".format(multiprocessing.current_process().pid))
print("PID of Main process is: {}".format(multiprocessing.current_process().pid))
P = multiprocessing.Process(target=Child_process)
P.start()
P.join()

Выход

PID of Main process is: 9401
PID of Child Process is: 9402

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

Мы можем создавать потоки, подклассифицируя класс threading.Thread . Кроме того, мы также можем создавать процессы, подклассифицируя класс multiprocessing.Process . Для использования процесса в подклассе нам необходимо учитывать следующие моменты:

  • Нам нужно определить новый подкласс класса Process .

  • Нам нужно переопределить класс _init_ (self [, args]) .

  • Нам нужно переопределить метод run (self [, args]), чтобы реализовать какой процесс

  • Нам нужно запустить процесс, вызвав метод start () .

Нам нужно определить новый подкласс класса Process .

Нам нужно переопределить класс _init_ (self [, args]) .

Нам нужно переопределить метод run (self [, args]), чтобы реализовать какой процесс

Нам нужно запустить процесс, вызвав метод start () .

пример

import multiprocessing
class MyProcess(multiprocessing.Process):
   def run(self):
   print ('called run method in process: %s' %self.name)
   return
if __name__ == '__main__':
   jobs = []
   for i in range(5):
   P = MyProcess()
   jobs.append(P)
   P.start()
   P.join()

Выход

called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5

Модуль многопроцессорной обработки Python — класс пула

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

применить () метод

Этот метод аналогичен методу .submit () класса .ThreadPoolExecutor. Он блокируется, пока результат не будет готов.

apply_async () метод

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

метод map ()

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

метод map_async ()

Это вариант метода map (), так как apply_async () относится к методу apply () . Возвращает объект результата. Когда результат становится готовым, к нему применяется вызываемый элемент. Призыв должен быть завершен немедленно; в противном случае поток, обрабатывающий результаты, будет заблокирован.

пример

Следующий пример поможет вам реализовать пул процессов для параллельного выполнения. Простое вычисление квадрата числа было выполнено путем применения функции square () с помощью метода multiprocessing.Pool . Затем pool.map () использовался для отправки 5, потому что input — это список целых чисел от 0 до 4. Результат будет сохранен в p_outputs и напечатан.

def square(n):
   result = n*n
   return result
if __name__ == '__main__':
   inputs = list(range(5))
   p = multiprocessing.Pool(processes = 4)
   p_outputs = pool.map(function_square, inputs)
   p.close()
   p.join()
   print ('Pool :', p_outputs)

Выход

Pool : [0, 1, 4, 9, 16]

Взаимодействие процессов

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

сношение

Различные коммуникационные механизмы

В этом разделе мы узнаем о различных коммуникационных механизмах. Механизмы описаны ниже —

Очереди

Очереди можно использовать с многопроцессорными программами. Класс Queue многопроцессорного модуля аналогичен классу Queue.Queue . Следовательно, можно использовать один и тот же API. Мультипроцессорная обработка .Queue предоставляет нам механизм взаимодействия потоков между процессами FIFO (первым пришел — первым обслужен).

пример

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

from multiprocessing import Process, Queue
import queue
import random
def f(q):
   q.put([42, None, 'hello'])
def main():
   q = Queue()
   p = Process(target = f, args = (q,))
   p.start()
   print (q.get())
if __name__ == '__main__':
   main()

Выход

[42, None, 'hello']

трубы

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

  • Он возвращает пару объектов соединения, которые представляют два конца канала.

  • У каждого объекта есть два метода — send () и recv () для взаимодействия между процессами.

Он возвращает пару объектов соединения, которые представляют два конца канала.

У каждого объекта есть два метода — send () и recv () для взаимодействия между процессами.

пример

Ниже приведен простой пример, взятый из официальных документов Python по многопроцессорности, для понимания концепции функции многопроцессорной обработки Pipe () .

from multiprocessing import Process, Pipe

def f(conn):
   conn.send([42, None, 'hello'])
   conn.close()

if __name__ == '__main__':
   parent_conn, child_conn = Pipe()
   p = Process(target = f, args = (child_conn,))
   p.start()
   print (parent_conn.recv())
   p.join()

Выход

[42, None, 'hello']

Менеджер

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

  • Основным свойством менеджера является управление серверным процессом, который управляет общими объектами.

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

Основным свойством менеджера является управление серверным процессом, который управляет общими объектами.

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

пример

Ниже приведен пример, который использует объект менеджера для создания записи списка в процессе сервера, а затем добавления новой записи в этот список.

import multiprocessing

def print_records(records):
   for record in records:
      print("Name: {0}\nScore: {1}\n".format(record[0], record[1]))

def insert_record(record, records):
   records.append(record)
      print("A New record is added\n")

if __name__ == '__main__':
   with multiprocessing.Manager() as manager:

      records = manager.list([('Computers', 1), ('Histoty', 5), ('Hindi',9)])
      new_record = ('English', 3)

      p1 = multiprocessing.Process(target = insert_record, args = (new_record, records))
      p2 = multiprocessing.Process(target = print_records, args = (records,))
	  p1.start()
      p1.join()
      p2.start()
      p2.join()

Выход

A New record is added

Name: Computers
Score: 1

Name: Histoty
Score: 5

Name: Hindi
Score: 9

Name: English
Score: 3

Концепция пространств имен в менеджере

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

пример

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

import multiprocessing

def Mng_NaSp(using_ns):

   using_ns.x +=5
   using_ns.y *= 10

if __name__ == '__main__':
   manager = multiprocessing.Manager()
   using_ns = manager.Namespace()
   using_ns.x = 1
   using_ns.y = 1

   print ('before', using_ns)
   p = multiprocessing.Process(target = Mng_NaSp, args = (using_ns,))
   p.start()
   p.join()
   print ('after', using_ns)

Выход

before Namespace(x = 1, y = 1)
after Namespace(x = 6, y = 10)

Ctypes-Array и Value

Многопроцессорный модуль предоставляет объекты Array и Value для хранения данных в карте общей памяти. Массив — это массив ctypes, выделенный из общей памяти, а Value — объект ctypes, выделенный из общей памяти.

Чтобы быть с, импортируйте Process, Value, Array из мультипроцессора.

пример

Следующий скрипт Python — это пример, взятый из документации по Python для использования массива Ctypes и Value для обмена некоторыми данными между процессами.

def f(n, a):
   n.value = 3.1415927
   for i in range(len(a)):
   a[i] = -a[i]

if __name__ == '__main__':
   num = Value('d', 0.0)
   arr = Array('i', range(10))

   p = Process(target = f, args = (num, arr))
   p.start()
   p.join()
   print (num.value)
   print (arr[:])

Выход

3.1415927
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]

Связь последовательных процессов (CSP)

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

Библиотека Python — PyCSP

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

PyCSP

В вышеупомянутой сети процессов PyCSP есть два процесса — Process1 и Process 2. Эти процессы взаимодействуют, передавая сообщения по двум каналам — каналу 1 и каналу 2.

Установка PyCSP

С помощью следующей команды мы можем установить библиотеку Python PyCSP —

pip install PyCSP

пример

Следующий скрипт Python — простой пример запуска двух процессов параллельно друг другу. Это делается с помощью библиотеки PyCSP python —

from pycsp.parallel import *
import time
@process
def P1():
   time.sleep(1)
   print('P1 exiting')
@process
def P2():
   time.sleep(1)
   print('P2 exiting')
def main():
   Parallel(P1(), P2())
   print('Terminating')
if __name__ == '__main__':
   main()

В приведенном выше сценарии две функции, а именно P1 и P2 , были созданы, а затем украшены @process для преобразования их в процессы.

Выход

P2 exiting
P1 exiting
Terminating

Событийное программирование

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

управляемый

Модуль Python — Asyncio

Модуль Asyncio был добавлен в Python 3.4 и предоставляет инфраструктуру для написания однопоточного параллельного кода с использованием сопрограмм. Ниже приведены различные концепции, используемые модулем Asyncio.

Цикл событий

Event-loop — это функциональность для обработки всех событий в вычислительном коде. Он действует во время выполнения всей программы и отслеживает входящие и выполняемые события. Модуль Asyncio допускает один цикл обработки событий для каждого процесса. Ниже приведены некоторые методы, предоставляемые модулем Asyncio для управления циклом событий:

  • loop = get_event_loop () — Этот метод предоставит цикл обработки событий для текущего контекста.

  • loop.call_later (time_delay, callback, аргумент) — этот метод организует обратный вызов, который должен быть вызван после заданных секунд time_delay.

  • loop.call_soon (callback, аргумент) — этот метод организует обратный вызов, который должен быть вызван как можно скорее. Обратный вызов вызывается после возврата call_soon () и когда элемент управления возвращается в цикл обработки событий.

  • loop.time () — Этот метод используется для возврата текущего времени в соответствии с внутренними часами цикла событий.

  • asyncio.set_event_loop () — этот метод устанавливает цикл обработки событий для текущего контекста в цикл.

  • asyncio.new_event_loop () — этот метод создает и возвращает новый объект цикла событий.

  • loop.run_forever () — этот метод будет работать до тех пор, пока не будет вызван метод stop ().

loop = get_event_loop () — Этот метод предоставит цикл обработки событий для текущего контекста.

loop.call_later (time_delay, callback, аргумент) — этот метод организует обратный вызов, который должен быть вызван после заданных секунд time_delay.

loop.call_soon (callback, аргумент) — этот метод организует обратный вызов, который должен быть вызван как можно скорее. Обратный вызов вызывается после возврата call_soon () и когда элемент управления возвращается в цикл обработки событий.

loop.time () — Этот метод используется для возврата текущего времени в соответствии с внутренними часами цикла событий.

asyncio.set_event_loop () — этот метод устанавливает цикл обработки событий для текущего контекста в цикл.

asyncio.new_event_loop () — этот метод создает и возвращает новый объект цикла событий.

loop.run_forever () — этот метод будет работать до тех пор, пока не будет вызван метод stop ().

пример

Следующий пример цикла обработки событий помогает в печати hello world с помощью метода get_event_loop (). Этот пример взят из официальных документов Python.

import asyncio

def hello_world(loop):
   print('Hello World')
   loop.stop()

loop = asyncio.get_event_loop()

loop.call_soon(hello_world, loop)

loop.run_forever()
loop.close()

Выход

Hello World

фьючерсы

Это совместимо с классом concurrent.futures.Future, который представляет вычисление, которое не было выполнено. Существуют следующие различия между asyncio.futures.Future и concurrent.futures.Future —

  • Методы result () и exception () не принимают аргумент timeout и вызывают исключение, когда будущее еще не сделано.

  • Обратные вызовы, зарегистрированные с помощью add_done_callback (), всегда вызываются через call_soon () цикла обработки событий.

  • Класс asyncio.futures.Future не совместим с функциями wait () и as_completed () в пакете concurrent.futures.

Методы result () и exception () не принимают аргумент timeout и вызывают исключение, когда будущее еще не сделано.

Обратные вызовы, зарегистрированные с помощью add_done_callback (), всегда вызываются через call_soon () цикла обработки событий.

Класс asyncio.futures.Future не совместим с функциями wait () и as_completed () в пакете concurrent.futures.

пример

Ниже приведен пример, который поможет вам понять, как использовать класс asyncio.futures.future.

import asyncio

async def Myoperation(future):
   await asyncio.sleep(2)
   future.set_result('Future Completed')

loop = asyncio.get_event_loop()
future = asyncio.Future()
asyncio.ensure_future(Myoperation(future))
try:
   loop.run_until_complete(future)
   print(future.result())
finally:
   loop.close()

Выход

Future Completed

Сопрограммы

Концепция сопрограмм в Asyncio аналогична концепции стандартного объекта Thread в модуле Threading. Это обобщение концепции подпрограммы. Сопрограмма может быть приостановлена ​​во время выполнения, чтобы она ожидала внешней обработки и возвращалась из точки, в которой она была остановлена, когда была выполнена внешняя обработка. Следующие два способа помогают нам в реализации сопрограмм —

функция асинхронной защиты ()

Это метод реализации сопрограмм в модуле Asyncio. Ниже приводится сценарий Python для того же самого —

import asyncio

async def Myoperation():
   print("First Coroutine")

loop = asyncio.get_event_loop()
try:
   loop.run_until_complete(Myoperation())

finally:
   loop.close()

Выход

First Coroutine

@ asyncio.coroutine decorator

Другой способ реализации сопрограмм — использование генераторов с декоратором @ asyncio.coroutine. Ниже приводится сценарий Python для того же самого —

import asyncio

@asyncio.coroutine
def Myoperation():
   print("First Coroutine")

loop = asyncio.get_event_loop()
try:
   loop.run_until_complete(Myoperation())

finally:
   loop.close()

Выход

First Coroutine

Задачи

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

import asyncio
import time
async def Task_ex(n):
   time.sleep(1)
   print("Processing {}".format(n))
async def Generator_task():
   for i in range(10):
      asyncio.ensure_future(Task_ex(i))
   int("Tasks Completed")
   asyncio.sleep(2)

loop = asyncio.get_event_loop()
loop.run_until_complete(Generator_task())
loop.close()

Выход

Tasks Completed
Processing 0
Processing 1
Processing 2
Processing 3
Processing 4
Processing 5
Processing 6
Processing 7
Processing 8
Processing 9

Транспорты

Модуль Asyncio предоставляет транспортные классы для реализации различных типов связи. Эти классы не являются поточно-ориентированными и всегда связаны с экземпляром протокола после установления канала связи.

Ниже приведены различные типы транспорта, унаследованные от BaseTransport —

  • ReadTransport — это интерфейс для транспортов только для чтения.

  • WriteTransport — это интерфейс для транспорта только для записи.

  • DatagramTransport — это интерфейс для отправки данных.

  • BaseSubprocessTransport — аналогично классу BaseTransport.

ReadTransport — это интерфейс для транспортов только для чтения.

WriteTransport — это интерфейс для транспорта только для записи.

DatagramTransport — это интерфейс для отправки данных.

BaseSubprocessTransport — аналогично классу BaseTransport.

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

  • close () — закрывает транспорт.

  • is_closing () — Этот метод вернет true, если транспорт закрывается или уже закрыт.transports.

  • get_extra_info (name, default = none) — это даст нам дополнительную информацию о транспорте.

  • get_protocol () — Этот метод возвращает текущий протокол.

close () — закрывает транспорт.

is_closing () — Этот метод вернет true, если транспорт закрывается или уже закрыт.transports.

get_extra_info (name, default = none) — это даст нам дополнительную информацию о транспорте.

get_protocol () — Этот метод возвращает текущий протокол.

протоколы

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

  • Протокол — это базовый класс для реализации потоковых протоколов для использования с транспортными средствами TCP и SSL.

  • DatagramProtocol — это базовый класс для реализации протоколов дейтаграмм для использования с транспортами UDP.

  • SubprocessProtocol — это базовый класс для реализации протоколов, взаимодействующих с дочерними процессами через набор однонаправленных каналов.

Протокол — это базовый класс для реализации потоковых протоколов для использования с транспортными средствами TCP и SSL.

DatagramProtocol — это базовый класс для реализации протоколов дейтаграмм для использования с транспортами UDP.

SubprocessProtocol — это базовый класс для реализации протоколов, взаимодействующих с дочерними процессами через набор однонаправленных каналов.

Реактивное программирование

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

ReactiveX или RX для реактивного программирования

ReactiveX или Raective Extension — самая известная реализация реактивного программирования. Работа ReactiveX зависит от следующих двух классов:

Наблюдаемый класс

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

Класс наблюдателя

Этот класс потребляет поток данных, испускаемый наблюдаемым . Может быть несколько наблюдателей с наблюдаемыми, и каждый наблюдатель будет получать каждый элемент данных, который испускается. Наблюдатель может получить три типа событий, подписавшись на наблюдаемые —

  • Событие on_next () — подразумевает наличие элемента в потоке данных.

  • Событие on_completed () — подразумевает завершение эмиссии, и больше ничего не приходит.

  • Событие on_error () — также подразумевает завершение эмиссии, но в случае, когда наблюдаемая выдает ошибку.

Событие on_next () — подразумевает наличие элемента в потоке данных.

Событие on_completed () — подразумевает завершение эмиссии, и больше ничего не приходит.

Событие on_error () — также подразумевает завершение эмиссии, но в случае, когда наблюдаемая выдает ошибку.

RxPY — модуль Python для реактивного программирования

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

pip install RxPY

пример

Ниже приведен скрипт Python, который использует модуль RxPY и его классы Observable и Observe для реактивного программирования. Есть в основном два класса —

  • get_strings () — для получения строк от наблюдателя.

  • PrintObserver () — для печати строк из наблюдателя. Он использует все три события класса наблюдателя. Он также использует класс subscribe ().

get_strings () — для получения строк от наблюдателя.

PrintObserver () — для печати строк из наблюдателя. Он использует все три события класса наблюдателя. Он также использует класс subscribe ().

from rx import Observable, Observer
def get_strings(observer):
   observer.on_next("Ram")
   observer.on_next("Mohan")
   observer.on_next("Shyam")
      observer.on_completed()
class PrintObserver(Observer):
   def on_next(self, value):
      print("Received {0}".format(value))
   def on_completed(self):
   print("Finished")
   def on_error(self, error):
      print("Error: {0}".format(error))
source = Observable.create(get_strings)
source.subscribe(PrintObserver())

Выход

Received Ram
Received Mohan
Received Shyam
Finished

PyFunctional библиотека для реактивного программирования

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

Разница между RxPY и PyFunctional

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

Установка PyFunctional модуля

Нам нужно установить этот модуль перед его использованием. Он может быть установлен с помощью команды pip следующим образом:

pip install pyfunctional

пример

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