Python thread daemon что это
Параллельные потоки в Python — Daemon Thread
В сегодняшнем посте мы рассмотрим использование потоков в Python. Прежде чем мы начнем с основной темы, давайте сначала посмотрим, что такое Daemon Thread.
Что такое Daemon Thread?
Daemon Thread в Python — это тип потока, который может работать независимо в фоновом режиме. Эти виды потоков выполняются независимо от основного потока. Так что они называются неблокирующими потоками.
Когда вам могут понадобиться потоки Daemon в Python?
Предположим, вам нужно иметь длительную задачу, которая пытается читать файлы журнала. Эта задача должна предупреждать пользователя при обнаружении сообщения об ошибке в журналах.
Мы можем назначить поток Daemon для этой задачи, который может отслеживать наши файлы журналов, в то время как наша основная программа выполняет свою обычную работу.
Самое полезное в потоках Daemon — это то, что они автоматически останавливают выполнение после завершения основной программы.
Если вам нужна короткая задача, поток Daemon прекратит выполнение после того, как вернется. Однако из-за этой природы потоки широко используются для длительных фоновых задач.
Практическая реализация
В этих примерах на Python будет использоваться модуль потоковой передачи, который является частью стандартной библиотеки.
Чтобы проиллюстрировать мощь потоков, давайте сначала создадим два потока, A и B.
Мы заставим поток A выполнить короткое вычисление, в то время как поток B будет пытаться отслеживать общий ресурс.
Обратите внимание, что оба потока являются обычными потоками.
Обратите внимание, что Daemon Thread не завершается. Это потому, что он будет автоматически убит основным потоком. Неблокирующая природа потоков делает их очень полезными для многих приложений Python.
Daemon Thread в Python
Чтобы начать работу с этим руководством, вы должны иметь базовые знания о потоках. В основном есть два типа, один из них – daemon, другой – non-daemon.
non-daemon, блокирует выход основной программы. daemon запускается, не блокируя выход из основной программы. И когда основная программа в Python завершается, связанные с ней потоки также уничтожаются.
Пример
У нас есть простая программа, в которой мы создаем два потока. Выполнение одного из них займет больше времени, потому что мы добавили 2 секунды сна. Давайте запустим приведенную ниже программу и посмотрим на результат.
Вы получите результат, как показано ниже.
Таким образом, оба потока выполняются, а затем основной поток завершает работу и программу.
Теперь мы сделаем Thread a, как daemon. Тогда вы увидите разницу в результатах. Итак, давайте отредактируем предыдущий код следующим образом.
Обратите внимание на дополнительный аргумент daemon = True при создании потока a. Вот как мы указываем поток, как Daemon Thread в Python. На изображении ниже показан результат работы программы.
Обратите внимание, что программа завершается, даже если Daemon Thread был запущен.
Когда нужен Daemon Thread?
В большом проекте некоторые потоки предназначены для выполнения фоновых задач, таких как отправка данных, периодическая сборка мусора и т.д. Это может быть выполнено no-Daemon. Но если используется no-Daemon, основной поток должен отслеживать их вручную. Однако, используя Daemon Thread, основной поток может полностью забыть об этой задаче, и эта задача будет либо завершена, либо уничтожена при выходе из основного потока.
Обратите внимание, что вы должны использовать Daemon Thread только для несущественных задач, когда вы не возражаете, если она не завершится или останется между ними.
Многопоточность в Python: модуль threading
Современное программное обеспечение проектируется так, что его функции и задачи могут выполняться параллельно. Python предоставляет программисту мощный набор инструментов для работы с потоками в библиотеке threading.
Как работает многопоточность
Многопоточность — это выполнение программы сразу в нескольких потоках, которые выполняют её функции одновременно.
Многопоточное программирование можно спутать с мультипроцессорным. На самом деле их концепции очень похожи, но если в первом случае программа работает с потоками, то в другом — с процессами. Разница между потоками и процессами проста: потоки имеют общую память, поэтому изменения в одном потоке видны в других, а процессы используют разные области памяти.
На самом деле, если рассмотреть одноядерный процессор, операции из разных потоков не выполняются параллельно. Одно ядро может выполнить только одну операцию в единицу времени, но так как операции выполняются очень быстро, создается ощущение параллельного выполнения, псевдопараллельность. По-настоящему параллельно программы могут работать только на многоядерных процессорах, где каждое ядро может выполнять операции независимо от других.
Отличным примером использования многопоточности является программа, где отрисовка графического интерфейса и обработка ввода пользователя управляются разными потоками. Если бы обе задачи были помещены в один поток, отрисовка интерфейса прерывалась бы каждый раз, когда программа получает ввод от пользователя. Использование двух потоков позволяет сделать выполнение этих функций независимым друг от друга.
Однако при выполнении многопоточной программы на одноядерном процессоре, её производительность будет ниже, чем если бы она была написана в один поток. Это происходит потому, что на реализацию и управление потоками тратится дополнительная память.
Можно ли считать threading многопоточным?
В Python используется GIL (Global Interpreter Lock), который однопоточный. Все потоки, которые создаются с помощью threading будут работать внутри потока GIL. В связи с этим они будут обрабатываться только одним ядром. Ни о какой работе одновременно на нескольких физических ядрах процессора не может быть и речи.
Но без него никуда не деться, если вам нужно выполнять несколько задач одновременно:
В чём преимущества тогда модуля Threading по сравнению с Multiprocessing? Рассмотрим их:
Подключение библиотеки threading
Threading – это стандартный модуль, который поставляется вместе с интерпретатором. Программисту не нужно устанавливать его, достаточно просто подключить модуль с помощью команды:
Здесь мы функцию mydef запустили в отдельном потоке. В качестве аргументов функции передали числа 1 и 2.
threading.Thread()
Эта конструкция позволяет создать новый поток, создав экземпляр класса Thread. Вот как выглядят её аргументы:
Она принимает аргументы:
Рассмотрим их подробнее:
Демоны
Демонами называют процессы, которые работают в фоновом режиме. В Python для демона есть более конкретное значение: демонический поток или поток демона. В отличие от обычных потоков поток демона автоматически завершает свою работу при закрытии программы. Иными словами, программа не будет ожидать завершения демонического потока, при её закрытии эти потоки уничтожаются, в каком бы состоянии они не находились.
Демонические потоки используют для выполнения операций, выполняемых в бесконечном цикле. В других случаях обычно используют простые потоки, которые задерживают закрытие программы, пока не завершат выполнение всех операций. Использование демонических потоков позволяет операции в фоновом режиме, которые обычно не связаны с изменением и сохранением долгосрочных данных.
Например, если программа полностью перезаписывает содержимое файла, и механизм перезаписи реализован в демоническом потоке, то при неожиданном выходе из программы данные потеряются.
В демонические потоки часто помещают функции по рисованию графического интерфейса. Рисование интерфейса — бесконечный процесс, который завершается сразу после выхода из программы, если просто поместить его в обычный поток, это будет препятствовать закрытию программы.
Методы для работы с потоками
Для создания и управления потоками используются различные методы класса Thread. С их помощью можно легко манипулировать сразу несколькими потоками и определять их поведение.
start()
Он используется для запуска созданного потока. После использования threading.Thread() создаётся новый поток, однако он неактивен. Для того чтобы он начал работу, используется метод start().
Этот метод блокирует выполнение потока, который его вызвал, до тех пор пока не завершится поток, метод которого был вызван. То есть если в потоке thread1 был вызван метод потока thread2: thread2.join(), то поток thread1 будет приостановлен до тех пор, пока выполнение thread2 не завершится.
С помощью этого метода можно заставить программу дождаться завершения демонического потока. Например, если вызвать метод в основном потоке, то программа не завершится, пока не выполнится демонический поток.
Если передать в качестве аргумента число, то для метода join() установится время ожидания, когда оно истечёт, поток продолжит свою работу.
Например, thr1.join(100) означает, что будет ожидаться завершение выполнения потока thr1 не более 100 секунд.
Так как метод join() всегда возвращает None, чтобы проверить, успел ли полностью выполниться поток за указанный timeout, нужно проверить, выполняется ли поток с помощью метода is_alive().
Здесь мы делаем поток демоническим, чтобы программа не дожидалась окончания выполнения функции. Подключаем модуль time, для того, чтобы сделать задержку в функции на 2.5 секунд. После старта потока, мы приостанавливаем основной поток на 0.125 секунд. Потом выполняем проверку is_alive(). Если выведет True, значит поток не закончил выполнение за 0.125 секунды.
В этом методе описываются операции, выполняемые потоком. Он используется, когда явно создается экземпляр класса. Пример:
is_alive()
Метод проверяет выполняется ли поток в данный момент. Его часто используют в связке с методом join(). Кроме того, с его помощью можно грамотно управлять выполнением потоков демонов, не позволяя им неожиданно завершить работу при закрытии программы, например:
Остановка потока
Бывают ситуации, когда требуется остановить поток, который работает в фоне. Допустим у нас поток у которого в функции run бесконечный цикл. В основной программе нам нужно его остановить. Тут самое простое — это создать некую переменную stop:
Вот пример такой программы:
Здесь используем глобальную переменную stop. Когда нам нужно остановить поток, мы ей присваиваем значение True, а дальше просто ждём его завершения.
Состояние гонки
Состояние гонки или race condition – это ошибка, возникающая при неправильном проектировании многопоточной программы. Она возникает тогда, когда несколько потоков обращаются к одним и тем же данным. Например, переменная хранит число, которое пытаются одновременно изменить потоки thread1 и thread2, что приводит к непредсказуемым результатам или ошибке.
Распространена ситуация, когда один поток проверяет значение переменной на выполнение условия, чтобы совершить какое-то действие, но между проверкой условия и выполнением действия вмешивается второй поток, который изменяет значение переменной, что приводит к получению неправильных результатов, например:
В итоге программа выведет сообщение: «При x = 5 функция 2*x = 2».
Состояние гонки может приводить к различным проблемам:
Доступ к общим ресурсам (lock)
Для того чтобы предотвратить состояние гонки, нужно использовать блокировку threading.Lock(), которая не позволяет сразу нескольким потокам работать с одними и теми же данными. Иными словами, Lock защищает данные от одновременного доступа.
threading.Lock() – возвращает объект, который, образно выражаясь, является дверью в комнату, которая запирается, если в комнате кто-то находится. То есть если поток использовал Lock (вошел в комнату), то другой поток вынужден ждать до тех пор, пока использовавший Lock поток не откажется от него (выйдет из комнаты).
У полученного объекта есть два метода: acquire() и release(). Рассмотрим их.
acquire()
Метод позволяет потоку получить блокировку. Имеет два аргумента: blocking и timeout.
Когда вызывается с аргументом blocking равным True (значение по умолчанию), блокирует Lock до тех пор, пока он не будет разблокирован и возвращает True. Если объект уже заблокирован, поток приостанавливается и ждёт, пока объект не будет разблокирован, а затем сам блокирует его.
При вызове с аргументов False, если объект Lock разблокирован, метод блокирует его и возвращает True. Если Lock уже заблокирован, метод ничего не делает и возвращает False.
release()
Этот метод разблокирует объект Lock. Интерпретатор позволяет вызывать его из любого потока, а не только из потока, который заблокировал Lock в данный момент.
Метод ничего не возвращает и вызывает ошибку RuntimeError, если вызывается, когда объект Lock уже разблокирован.
Здесь мы создаём объект lock, с его помощью мы будем безопасно считывать и изменять данные. В качестве данных, которые мы будем блокировать в данном примере это одна переменная x. Далее показано безопасное изменение данных: вначале с помощью acquire дожидаемся своей очереди доступа к ним. Затем изменяем их (в нашем примере перезаписываем значение переменной с «Python 2» на «Python 3»). Далее выводим значение в консоль. После этого освобождаем доступ для других потоков. Если все потоки, которым нужен будет доступ к данным x будут использовать lock, то можно избежать «Состояния гонки».
deadlock
При использовании Lock возникает серьезная проблема, которая приводит к полной остановки работы программы. Если вызвать метод acquire(), а объект Lock уже заблокирован, то вызвавший acquire() поток будет ждать, пока заблокировавший объект поток не вызовет release().
Если один поток вызывает метод блокировки несколько раз подряд, то выполнение потока приостанавливается, пока он сам не вызовет release(). Однако он не может вызвать release, потому что его выполнение приостановлено, что означает бесконечную блокировку программы.
Самоблокировку можно предотвратить, если удалить лишний вызов acquire(), но это не всегда возможно. Самоблокировка может происходить из-за следующий вещей:
В случае возникновения ошибок достаточно воспользоваться конструкцией try-finally или оператором with.
Вот пример с with:
Конструкция try-finally позволяет удалять блокировку даже в случае возникновения ошибок, что позволяет избежать deadblock. Пример:
Конструкция try-finally гарантирует, что код в finally будет исполнен всегда, независимо от ошибок и результатов блока try.
Однако это не работает в случае самоблокировки из-за неправильного проектирования программы. Для этого был создан объект RLock.
RLock
Если Lock заблокирован, он блокирует любой поток, попытавшийся сделать то же самое, даже если этот поток и является владельцем блокировки в данный момент. Например, программист написал код:
RLock блокирует поток только в том случае, если объект заблокирован другим потоком. Используя RLock, поток никогда не сможет заблокировать сам себя.
Использовать RLock нужно для управления вложенным доступом к разделяемым объектам. Чтобы решить возникшую проблему с Lock в коде выше, достаточно заменить строчку « lock1 = threading.Lock() » на « lock1 = threading.RLock() ».
Передача данных с помощью очередей (Queue)
Для передачи данных с помощью очередей используется класс Queue из библиотеки queue, который импортируется командной: «from queue import Queue».
Библиотеке queue содержит все необходимые инструменты для передачи данных между потоками и реализует нужные механизмы блокировки.
Класс Queue реализует очередь FIFO, который работает так: первый элемент, который пошел в очередь, первым и выйдет из неё. Эту очередь можно сравнить с вертикальной полой трубой, в которую сверху бросают элементы.
Queue имеет параметр maxsize, принимающий только целочисленные значения. Он указывает максимальное количество элементов, которое можно поместить в очередь. Когда максимум достигается, добавление в очередь элементов блокируется, пока в ней не освободится место. Если maxsize принимает значение myfunc выполнится через 4 секунды после вызова метода start().
Barrier
Этот класс позволяет реализовать простой механизм синхронизации потоков. Его можно использовать для фиксированного числа потоков, когда необходимо, чтобы каждый поток ждал выполнения какого-либо действия всеми.
Для того чтобы продолжить выполнения, все потоки должны вызвать метод wait(), если хоть один поток не сделал этого, остальные блокируются до тех пор, пока метод не будет вызван.
Так выглядят его аргументы:
Рассмотрим пример использования:
Здесь выставляю barrier на 2 вызова wait. То есть, для того, чтобы выполнился код после wait, wait должен быть вызван в 2 потоках. В данном случае функция myfunc сразу запускается в потоке, но она сразу не выведет ‘отработал barrier’ в консоль, а дождётся когда в основном потоке будет вызван wait тоже.
Event
Event представляет собой простой механизм реализации связи между потоками: один поток даёт сигнал о событии, другие ожидают этого сигнала.
Объект события управляет внутренним флагом, который может быть установлен в True или False с помощью методов set() и clear(). Также есть методы is_set(), которым можно проверить состояние внутреннего флага. С помощью метода wait(timeout=None) можно ждать пока не выставлен флаг в True. Так же при необходимости можно задать время ожидания.
Вот пример:
Заключение
Возможность управления потоками в Python – это мощный инструмент в разработке больших программ. Для работы с ними используется модуль Threading и библиотека queue в связке с ним.
Каждый программист Python должен уметь работать с потоками, очередями и понимать, как устроена блокировка, доступ к данным и их передача между потоками.
Потоки демонов в Python – Что это такое и как их создавать?
Привет всем! В сегодняшнем посте мы рассмотрим использование потоков демонов в Python. Прежде чем мы начнем с основной темы, давайте посмотрим, что такое демон
Привет всем! В сегодняшнем посте мы рассмотрим использование потоков демонов в Python. Прежде чем мы начнем с основной темы, давайте сначала посмотрим, что такое поток демонов!
Потоки демонов
Поток демона – это тип потока, который может работать независимо в фоновом режиме. Эти типы потоков выполняются независимо от основного потока. Поэтому они называются неблокирующими потоками.
Когда вам могут понадобиться потоки демонов в Python?
Предположим, вам нужна длительная задача, которая пытается прочитать файлы журналов. Эта задача должна предупреждать пользователя при обнаружении сообщения об ошибке в журналах.
Мы можем назначить поток демона для этой задачи, который может продолжать отслеживать ваши файлы журналов, в то время как наша основная программа выполняет свою обычную работу!
Самое лучшее в потоках демонов заключается в том, что они автоматически останавливают выполнение после завершения основной программы!
В случае, если вам нужна короткая задача, поток демона остановит выполнение после ее возвращения. Однако из-за такой природы потоки демонов широко используются для длительных фоновых задач.
Теперь давайте рассмотрим пример, который показывает, как мы можем использовать их в Python!
Использование потоков демонов в Python – Практическая реализация
Чтобы проиллюстрировать мощь потоков демонов, давайте сначала создадим два потока, A и B.
Мы заставим поток A выполнить короткое вычисление, в то время как поток B попытается контролировать общий ресурс.
Обратите внимание, что оба потока являются обычными потоками. Теперь давайте сделаем поток B потоком демона. Посмотрим, что будет дальше.
Здесь обратите внимание, что поток демона не завершается. Это происходит потому, что он автоматически будет убит основным потоком.
Неблокирующий характер потоков демонов делает его очень полезным для многих приложений Python.
Вывод
В этой статье мы узнали о том, как мы можем использовать потоки демонов в нашем приложении Python
Цель: | Создание модуля thread для простого управления несколькими потоками выполнения. |
Доступно в версии: | 1.5.2 и выше. |
Модуль threading Python 3 построен на низкоуровневых функциях thread, что делает работу с потоками проще. Потоки в Python позволяет программе запускать одновременно несколько операций в одном пространстве процесса.
Объекты потоков
Результат работы программы – пять строк со строкой «Worker»:
В приведенном ниже примере в качестве аргумента потоку передается число для вывода.
Целочисленный аргумент теперь включен в сообщение, выводимое каждым потоком:
Определение текущего потока
Использование аргументов для идентификации потока является трудоемким процессом. Каждый экземпляр Thread имеет имя со значением, присваиваемым по умолчанию. Оно может быть изменено, когда создается поток.
Именование потоков полезно в серверных процессах с несколькими служебными потоками, обрабатывающими различные операции.
Большинство программ не используют print для отладки. Модуль logging поддерживает добавление имени потока в каждое сообщение журнала с помощью % (threadName)s. Включение имен потоков в журнал облегчает отслеживание этих сообщений.
Модуль logging также является поточно-ориентированным, поэтому сообщения из разных потоков сохранятся в выводимых данных.
Daemon потоки non-daemon
До этого момента примеры программ ожидали, пока все потоки не завершат свою работу. Иногда программы порождают такой поток, как демон. Он работает, не блокируя завершение основной программы.
Использование демона полезно, если не удается прервать поток или завершить его в середине работы, не потеряв и не повредив при этом данные.
Чтобы пометить поток как demon, вызовите метод setDaemon() с логическим аргументом. По умолчанию потоки не являются «демонами», поэтому передача в качестве аргумента значения True включает режим demon.
Обратите внимание, что в выводимых данных отсутствует сообщение «Exiting» от потока-демона. Все потоки, не являющиеся «демонами» (включая основной поток), завершают работу до того, как поток-демон выйдет из двухсекундного сна.
Метод join() позволяет demon вывести сообщение «Exiting».
Также можно передать аргумент задержки (количество секунд, в течение которых поток будет неактивным).
Истекшее время ожидания меньше, чем время, в течение которого поток-демон спит. Поэтому поток все еще «жив» после того, как метод join() продолжит свою работу.
Нумерация потоков
Поскольку worker спит в течение случайного отрезка времени, выходные данные программы могут отличаться. Это должно выглядеть примерно так:
Подклассы потоков
Возвращаемое значение метода run() игнорируется.
Для передачи аргументов пользовательскому потоку, переопределите конструктор, чтобы сохранить значения в атрибуте экземпляра.
Таймеры потока
Timer начинает свою работу после задержки и может быть отменен в любой момент этой задержки.
Второй таймер никогда не запускается, а первый запускается после завершения работы основной программы. Поскольку это не поток-демона, он присоединяется неявно, когда основной поток завершен.
Сигналы между потоками
Контроль доступа к ресурсам
Помимо синхронизации операций с потоками, также важно иметь возможность контролировать доступ к общим ресурсам, чтобы предотвратить повреждение данных.
В следующем примере worker() пытается применить блокировку три раза и подсчитывает, сколько попыток нужно сделать. А lock_holder() выполняет циклическое переключение между снятием и запуском блокировки с короткими паузами в каждом состоянии, используемом для имитации загрузки.
worker() требуется более трех итераций, чтобы применить блокировку три раза.
Повторные блокировки
Обычные объекты Lock не могут быть получены более одного раза даже одним и тем же потоком. Это может привести к нежелательным эффектам, если доступ к блокировке осуществляется несколькими функциями в одной цепочке вызовов.
Блокировки как менеджеры контекста
Функции worker_with() и worker_no_with() управляют блокировкой эквивалентными способами.
Синхронизация потоков
Ограничение одновременного доступа к ресурсам
Как разрешить доступ к ресурсу нескольким worker одновременно, но при этом ограничить их количество. Например, пул соединений может поддерживать фиксированное число одновременных подключений, или сетевое приложение может поддерживать фиксированное количество одновременных загрузок. Semaphore является одним из способов управления соединениями.
В этом примере класс ActivePool является удобным способом отслеживания того, какие потоки могут запускаться в данный момент. Реальный пул ресурсов будет выделять соединение для нового потока и восстанавливать значение, когда поток завершен. В данном случае он используется для хранения имен активных потоков, чтобы показать, что только пять из них работают одновременно.
Специфичные для потока данные
Некоторые ресурсы должны быть заблокированы, чтобы их могли использовать сразу несколько потоков. А другие должны быть защищены от просмотра в потоках, которые не «владеют» ими. Функция local() создает объект, способный скрывать значения для отдельных потоков.
Обратите внимание, что значение local_data.value не доступно ни для одного потока, пока не будет установлено.
__init __() вызывается для каждого объекта (обратите внимание на значение id() ) один раз в каждом потоке.
Дайте знать, что вы думаете по данной теме статьи в комментариях. Мы крайне благодарны вам за ваши комментарии, отклики, дизлайки, лайки, подписки!
Пожалуйста, опубликуйте ваши отзывы по текущей теме статьи. Мы очень благодарим вас за ваши комментарии, отклики, лайки, подписки, дизлайки!