Как сделать многопоточность в python

Добавил пользователь Евгений Кузнецов
Обновлено: 19.09.2024

Python имеет ужасную репутацию, когда речь идет о возможности параллельных вычислений. Не обращая внимания на типичные рассуждения о его потоках и GIL (который обычно нормально работает), по-моему реальная проблема многопоточности Python не техническая, а педагогическая. Распространенные руководства о библиотеках threading и multiprocessing в целом неплохие, но тяжеловаты для понимания. Они начинаются с глубоких вещей, и заканчиваются до просто применяемых практик.

Традиционный пример

Беглое ознакомление с первыми результатами поискового запроса на тему “Python threading tutorial” показывает, что почти каждый из них основан на использовании какого-либо вспомогательного класса в связке с модулем Queue.

Типичный пример многопоточности вида поставщик-потребитель:

Мда… Просматриваются Java’вские корни.

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

Проблемы (на мой взгляд)

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

Больше воркеров, больше задач

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

Работает отлично, но посмотрите на весь этот код! Здесь методы инициализации, списки потоков для отслеживания работы, и что хуже всего, если вы склонны к обработке блокировок как и я, куча вызовов метода join. А впоследствии будет еще сложнее!

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

Знакомьтесь: Map

Map - это класная маленькая функция, а главное, проста для распараллеливания вашего Python кода. Для тех, кто не вкурсе, map заимствована из функциональных языков, вроде Lisp’а. Это функция, которая применяет другую функцию к последовательности, например:

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

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

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

Функция map с поддержкой многопоточности присутствует в двух библиотеках: multiprocessing, а так же малоизвестная, но неменее замечательная - multiprocessing.dummy.

Отступление: Что это? Никогда не слышал о многопоточном клоне библиотеки multiprocessing под названием dummy? Я тоже не слышал до недавнего времени. Есть всего одно предложение на странице официальной документации библиотеки multiprocessing. И это предложение сводится к “Ах да, эта вещь существует”. Это печально, скажуя вам!

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

Приступим

Для доступа к map-параллельной функции, сперва нужно импортировать модули в которых она содержится и создать пулл:

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

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

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

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

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

Посмотрите на это! Код который на самом деле работает занимает 4 строки, 3 из которых формальны. Функция map сделала то же, что и предыдущий код в 40 строк с такой легкостью! Для проверки я испробовал оба подхода и попробовал различные размеры пула.

Результаты:

Потрясающе! Это так же показывает, почему полезно поэкспериментировать с размером пула. Любой пулл с более чем 9 воркерами быстро приводит в падению прироста скорости (на этом компе).

Создание миниатюр для тысяч изображений

Теперь давайте сделаем что-нибудь процесорно-раздельное! Довольно распространенная задача у меня на работе - это обработка больших коллекций картинок. Одна из таких задач - создание миниатюр. И это можно распараллелить.

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

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

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

Если мы заменим цикл for параллельной функцией map:

5.6 секунд!

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

Редакция Кодкампа

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

target параметр ссылается на функцию (или вызываемый объект) , который будет работать. Нить не начнется выполнение до start не вызывается на Thread объекта.

Начало темы

Теперь, когда my_thread побежал и завершается, вызов start снова будет производить RuntimeError . Если вы хотите , чтобы запустить нить , как демон, передавая daemon=True kwarg или установить my_thread.daemon в True перед вызовом start() , вызывает ваш Thread запустить в фоновом режиме , как демон.

Присоединение к теме

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

Например, допустим, вы хотите загрузить несколько страниц веб-сайта и собрать их в одну страницу. Вы бы сделали это:

Более пристальный взгляд на то, как join() работает , можно найти здесь .

Создать пользовательский класс потока

Использование threading.Thread класса мы можем создать подкласс нового пользовательского класса Thread. мы должны переопределить run метод в подклассе.

Общение между потоками

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

Вы можете использовать Queue из queue библиотеки.

Создание потоков производителей и потребителей с общей очередью

Создание рабочего пула

Использование threading & queue :

Расширенное использование многопоточности

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

Усовершенствованный принтер (регистратор)

Остановляемая нить с петлей

На основе этого вопроса .

Синтаксис

Параметры

Примечания

Тренажер

Научим основам Python и Data Science на практике

Это не обычный теоритический курс, а онлайн-тренажер, с практикой на примерах рабочих задач, в котором вы можете учиться в любое удобное время 24/7. Вы получите реальный опыт, разрабатывая качественный код и анализируя реальные данные.

Параллельные вычисления

Введение Примеры Использование многопроцессорного модуля для распараллеливания задач import multiprocessing def fib(n): """computing the Fibonacci in an inefficient way was chosen to slow down the CPU."""

Написание расширений (extensions) для Python на C


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

Если вам нужно сделать что-то, для чего нужен интенсивный CPU, тогда вам, возможно, захочется взглянуть на модуль multiprocessing, вместо threading. Причина заключается в том, что Python содержит Global Interpreter Lock (GIL), который запускает все потоки внутри главного потока. По этой причине, когда вам нужно запустить несколько интенсивных операций с потоками, вы заметите, что все работает достаточно медленно. Так что мы сфокусируемся на том, в чем потоки являются лучшими: операции I/O.

Небольшое интро

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

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

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

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

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

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


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

В этом примере мы только что унаследовали класс threading.Thread. Мы передали число, которое хотим удвоить, а также передали объект логгированмя, как делали это ранее. Но на этот раз, мы настроим название потока по-другому, вызвав функцию setName в объекте потока. Нам все еще нужно вызвать старт в каждом потоке, но запомните, что нам не нужно определять это в наследуемом классе. Когда вы вызываете старт, он запускает ваш поток, вызывая метод run. В нашем классе мы вызываем функцию doubler для выполнения наших вычислений. Выдача сильно похожа на ту, что была в примере ранее, за исключением того, что я добавил дополнительную строку в выдаче. Попробуйте сами и посмотрите, что получится.

Замки и Синхронизация

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

Решение проблемы – это использовать замки. Замок предоставлен модулем Python threading и может держать один поток, или не держать поток вообще. Если поток пытается acquire замок на ресурсе, который уже закрыт, этот поток будет ожидать до тех пор, пока замок не откроется. Давайте посмотрим на практичный пример одного кода, который не имеет никакого замочного функционала, но мы попробуем его добавить:

Мы можем сделать этот пример еще интереснее, добавив вызов time.sleep. Следовательно, проблема здесь в том, что один поток может вызывать update_total и перед тем, как он обновится, другой поток может вызвать его и тоже попытается обновить его. В зависимости от порядка операций, значение может быть добавлено единожды. Давайте добавим замок к функции. Существует два способа сделать эта. Первый – это использование try/finally, если мы хотим убедиться, что замок снят. Вот пример:

Здесь мы просто вешаем замок, перед тем как сделать что-либо другое. Далее, мы пытаемся обновить total и finally, мы снимаем замок и выводим нынешний total. Мы можем упростить данную задачу, используя оператор Python под названием with:

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

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

Когда вы запустите этот код, вы увидите, что он просто висит. Причина в том, что мы просто указываем модулю threading повесить замок. Так что когда мы вызываем первую функцию, она видит, что замок уже висит и блокируется. Это будет длиться до тех пор, пока замок не снимут, что никогда и не случится, так как это не предусмотрено в коде. Хорошее решение в данном случае – использовать re-entrant замок. Модуль threading предоставляет такой, в виде функции RLock. Просто замените строку lock = threading.Lock() на lock = threading.RLock() и попробуйте перезапустить код. Теперь он должен заработать. Если вы хотите попробовать код выше но добавить в него потоки, то мы можем заменить call на main следующим образом:

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

Таймеры

Модуль threading включает в себя один очень удобный класс, под названием Timer, который вы можете использовать запуска действия, спустя определенный отрезок времени. Данный класс запускает собственный поток и начинают работу с того же метода start(), как и обычные потоки. Вы также можете остановить таймер, используя метод cancel. Обратите внимание на то, что вы можете отменить таймер еще до того, как он стартовал. Однажды у меня был случай, когда мне нужно было наладить связь с под-процессом, который я начал, но мне нужен было обратный отсчет. Несмотря на существования ряда различных способов решения этой отдельной проблемы, моим любимым решением всегда было использование класса Timer модуля threading. Для этого примера мы взглянем на применение команды ping. В Linux, команда ping будет работать, пока вы её не убьете. Так что класс Timer становится особенно полезным для мира Linux. Вот пример:

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

Другие Компоненты Потоков

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

Еще один полезный инструмент, который содержится в модуле, это Event. С его помощью вы можете получить связь между двумя потоками, используя сигналы. Мы рассмотрим примеры применения Event в следующей статье. Наконец-то, в версии Python 3.2 был добавлен объект Barrier. Это примитив, который управляет пулом потока, при этом не важно, где потоки должны ждать своей очереди. Для передачи барьера, потоку нужно вызвать метод wait(), который будет блокировать до тех пор, пока все потоки не сделают вызов. После чего все потоки пройдут дальше одновременно.

Связь потоков

Существует ряд случаев, когда вам нужно сделать так, чтобы потоки были связанны друг с другом. Как я упоминал ранее, вы можете использовать Event для этой цели. Но более удобный способ – использовать Queue. В нашем примере мы используем оба способа! Давайте посмотрим, как это будет выглядеть:

Давайте немного притормозим. Во первых, у нас есть функция creator (также известная, как producer), которую мы используем для создания данных, с которыми мы хотим работать (или использовать). Далее мы получаем еще одну функцию, которую мы используем для обработки данных, под названием my_consumer. Функция creator использует метод Queue под названием put, чтобы добавить данные в очередь, затем потребитель, в свою очередь, будет проверять, есть ли новые данные и обрабатывать их, когда такие появятся. Queue обрабатывает все закрытия и открытия замков, так что лично вам эта участь не грозит.

В данном примере мы создали список значений, которые мы хотим дублировать. Далее мы создаем два потока, один для функции creator/producer, второй для consumer (потребитель). Обратите внимание на то, что мы передаем объект Queue каждому потоку, что является прямо таки магией, учитывая то, как обрабатываются замки. Очередь начнется с первого потока, который передает данные второму. Когда первый поток передает те или иные данные в очередь, он также передает их к Event, после чего дожидается, когда произойдет события, чтобы закончить. Далее, в функции consumer, данные обрабатываются, и после этого вызывается метод настройки Event, который указывает первому потоку, что второй закончил обработку, так что он может продолжать. Последняя строка кода вызывает метод join объекта Queue, который указывает Queue подождать, пока потоки закончат обработку. Первый поток заканчивает, когда ему больше нечего передавать в Queue.

Подведем итоги

Мы рассмотрели достаточно много материала. А именно:

  1. Основы работы с модулем threading
  2. Как работают замки
  3. Что такое Event и как его можно использовать
  4. Как использовать таймер
  5. Внутрипотоковая связь с использованием Queue/Event

Теперь вы знаете, как использовать потоки, и в чем они хороши. Надеюсь, вы найдете им применение в своем собственном коде!

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

Что такое тема?

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

Что такое процесс?

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

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

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

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

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

Многопоточность Python против многопроцессорности

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

Каждый процесс будет иметь 2 основных компонента:

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

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

В этом уроке вы узнаете,

Зачем использовать многопоточность?

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

Python MultiThreading

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

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

Модули Thread и Threading

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

Тем не менее, модуль потока давно устарел. Начиная с Python 3, он был обозначен как устаревший и доступен только как __thread для обратной совместимости.

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

Модуль потока

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

Хорошо, теперь вы рассмотрели базовую теорию, чтобы начать кодирование. Итак, откройте свой IDLE или блокнот и введите следующее:

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



ОБЪЯСНЕНИЕ КОДА

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

Поточный модуль

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


Вот список некоторых полезных функций, определенных в этом модуле:

Имя функции Описание
activeCount () Возвращает количество объектов Thread, которые еще живы
currentThread () Возвращает текущий объект класса Thread.
перечисление () Перечисляет все активные объекты Thread.
isDaemon () Возвращает true, если поток является демоном.
является живым() Возвращает true, если поток еще жив.
Методы класса Thread
Начните() Запускает активность потока. Он должен вызываться только один раз для каждого потока, потому что он вызовет ошибку времени выполнения, если вызывается несколько раз.
запустить() Этот метод обозначает активность потока и может быть переопределен классом, который расширяет класс Thread.
присоединиться() Он блокирует выполнение другого кода до тех пор, пока поток, в котором был вызван метод join (), не завершится.

Предыстория: класс Thread

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

Наиболее распространенный способ создания многопоточного приложения на Python — это объявление класса, который расширяет класс Thread и переопределяет его метод run ().

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

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

  1. определить класс, который расширяет класс Thread
  2. Переопределить конструктор __init__
  3. Переопределение прогона () метод

Как только объект потока создан, метод start () может использоваться для начала выполнения этого действия, а метод join () может использоваться для блокировки всего другого кода до завершения текущего действия.

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

Это будет вывод, когда вы выполните приведенный выше код:


ОБЪЯСНЕНИЕ КОДА


  1. Эта часть такая же, как в нашем предыдущем примере. Здесь вы импортируете модуль времени и потока, который используется для обработки выполнения и задержек потоков Python.
  2. В этом бите вы создаете класс с именем threadtester, который наследует или расширяет класс Thread модуля Threading. Это один из самых распространенных способов создания потоков в Python. Однако вам следует переопределить только конструктор и метод run () в вашем приложении. Как вы можете видеть в приведенном выше примере кода, метод __init__ (конструктор) был переопределен.

Точно так же вы также переопределили метод run () . Он содержит код, который вы хотите выполнить внутри потока. В этом примере вы вызвали функцию thread_test ().

Здесь мы создаем поток и передаем три параметра, которые мы объявили в __init__. Первый параметр — это идентификатор потока, второй параметр — имя потока, а третий параметр — счетчик, который определяет, сколько раз должен выполняться цикл while.

Метод start используется для запуска выполнения потока. Внутри функция start () вызывает метод run () вашего класса.

Метод join () блокирует выполнение другого кода и ожидает завершения потока, в котором он был вызван.

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

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

Тупики и условия гонки

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

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

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

Тупики

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

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

Пять философов сидят за круглым столом с пятью тарелками спагетти (разновидность макарон) и пятью вилками, как показано на схеме.


В любой момент времени философ должен либо есть, либо думать.

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

Поскольку у каждого из философов есть одна вилка, они все будут ждать, пока другие положат свою вилку. В результате никто из них не сможет съесть спагетти.

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

Условия гонки

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

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

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

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

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

Когда состояние блокировки разблокировано, вызов метода acqu () изменит состояние на заблокированное и вернет его. Однако, если состояние заблокировано, вызов acqu () блокируется до тех пор, пока метод release () не будет вызван каким-либо другим потоком.

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

Вот пример использования блокировок в ваших приложениях. Запустите ваш IDLE и введите следующее:

Теперь нажмите F5. Вы должны увидеть результат вроде этого:


ОБЪЯСНЕНИЕ КОДА


Теория в порядке, но откуда вы знаете, что блокировка действительно работает? Если вы посмотрите на вывод, вы увидите, что каждый из операторов печати печатает ровно одну строку за раз. Напомним, что в более раннем примере выходные данные из print были случайными, потому что несколько потоков обращались к методу print () одновременно. Здесь функция печати вызывается только после получения блокировки. Таким образом, выходные данные отображаются по одному и построчно.

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

  1. RLocks
  2. семафоры
  3. условия
  4. События и
  5. барьеры

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

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

  1. Код с привязкой к ЦП: это относится к любому коду, который будет непосредственно выполняться ЦП.
  2. Код, связанный с вводом / выводом: это может быть любой код, который обращается к файловой системе через ОС
  3. CPython: это эталонная реализация Python и может быть описана как интерпретатор, написанный на C и Python (язык программирования).

Что такое GIL?

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

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

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

  1. Интерпретатор Python создает новый процесс и порождает потоки
  2. Когда поток 1 начинает работать, он сначала получает GIL и блокирует его.
  3. Если поток 2 хочет выполнить сейчас, ему придется ждать освобождения GIL, даже если другой процессор свободен.
  4. Теперь предположим, что поток-1 ожидает операции ввода-вывода. В это время он выпустит GIL, а thread-2 получит его.
  5. После завершения операций ввода-вывода, если поток-1 хочет выполнить сейчас, ему снова придется ждать, пока поток-GIL не будет выпущен потоком-2.

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

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

Зачем нужен GIL?

Сборщик мусора CPython использует эффективную технику управления памятью, известную как подсчет ссылок. Вот как это работает: Каждый объект в Python имеет счетчик ссылок, который увеличивается, когда он присваивается новому имени переменной или добавляется в контейнер (например, кортежи, списки и т. Д.). Аналогично, счетчик ссылок уменьшается, когда ссылка выходит из области видимости или когда вызывается оператор del. Когда счетчик ссылок объекта достигает 0, он подвергается сборке мусора, и выделенная память освобождается.

Но проблема в том, что переменная подсчета ссылок склонна к условиям гонки, как и любая другая глобальная переменная. Чтобы решить эту проблему, разработчики python решили использовать глобальную блокировку интерпретатора. Другим вариантом было добавить блокировку к каждому объекту, что привело бы к взаимным блокировкам и увеличению накладных расходов от вызовов acqu () и release ().

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

Читайте также: