Connect with us

Обучение

Заметки Python #23: Потоки, процессы

724

 

Потоки в Python — одна из самых обсуждаемых вещей на профильных ресурсах.

Рассмотрим, что такое потоки и почему многопоточность в питоне — бессмысленная трата времени

Многопоточность  — нет, мультипроцессорность — да

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

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

Реализовать многопоточность в питоне, так или иначе, можно, но из-за GIL запуск на нескольких процессоров всё равно не будет осуществлен — все потоки будут работать только на одном процессоре — это не даст никаких преимуществ

Модуль threading

Класс Thread из модуля threading позволяет запускать программу в отдельном потоке. Так выглядит его простейшая реализация:

Разбираем пример.

1. аrgs — это кортеж, сюда передаем позиционные аргументы функции.

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

3. start — запуск потока на выполнение (внутри target — это название функции, которая будет выполняться)

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


 

Поток в виде класса

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

Результат:

Разбираем:

1. super().__init__ — мы вызываем конструктор родителя (Thread) базового класса, чтобы использовать все методы

2. start () запускает сам поток, а в run() мы запишем непосредственно блок кода для выполнения

События

 

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

 

Результат:

Блокировки

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

Если один поток выполняет операции с данными, то второй поток должен быть заблокирован. Блокировка — объект, экземпляр класса Lock(). Есть пример синхронизации потоков при помощи блокировок (мьютексов) показано, как будет работать потоки с блокировкой и без. В идеале — у нас всегда должен быть ноль

 

 

Результат:

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

Что мы использовали в примере:

1. acquire() — приобрести блокировку

2. release() — освободить блокировку

Семафоры

Еще один механизм синхронизации потоков. Семафор — класс, значение по умолчанию равно нулю, это некий внутренний счетчик. Работает аналогично событиям — уменьшается при каждом вызове метода acquire() и увеличивается при каждом вызове release(). Если счетчик семафора достигает нуля, метод acquire() приостанавливает работу потока, пока другой поток не вызовет release(). Как выглядит синхронизация потоков при помощи семафоров?

Результат:

 

Что использовали в примере:

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

2. release() — увеличивает внутренний счетчик семафора на 1. Если перед вызовом метода счетчик был равен нулю и имеется другой поток, ожидающий освобождения семафора, этот поток возобновляет работу

Модуль multiprocessing

С его помощью можно запускать задачи в каждом отдельном процессе. Интерфейс этого модуля имитирует потоки, но процессы не работают с общими данными. Модуль multiprocessing позволяет запускать процессы, которые синхронизируются между собой на нескольких процессорах. Как они взаимодействуют друг с другом? С помощью объектов очереди multiprocessing.JoinableQueue(). Самый простой пример (без очередей)

Результат:

В следующем примере мы реализуем в виде класса не поток, а подпроцесс (субпроцесс). Наследуемся от базового класса — multiprocessing.Process

Результат:

В чем плюс использования процесса, а не потока?

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

Синхронизация процессов в модуле multiprocessing. Очереди Queue()

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

Сервер должен в цикле перебирать заготовки, обрабатывать их и (put()) поместить обработанную заготовку в очередь. ПОТРЕБИТЕЛЬ — При появлении в очереди объекта, извлечь его, обработать и как-то объявить поставщику, что мы его получили

Результат:

Что использовали в примере:

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

2. put() — добавляет элемент в очередь. Если она заполнена до предела, процесс приостанавливается до появления места.

3. multiprocessing.JoinableQueue() — используется потребителем, чтобы сообщить, что элемент очереди, полученный методом get(), был обработан. Вызывает исключение ValueError, если количество вызовов этого метода превышает число элементов, извлеченных из очереди

Нажмите что бы оставить комментарий

Ответить

Ваш e-mail не будет опубликован.

Лучшие сервисы стриминга музыки в 2019 году

Сервисы

Телевидение Wink Ростелеком: Samsung LG, Sony, Phillips, Android TV

Ростелеком

Ноутбуки Asus не видят жесткий диск. Автоматический вход в BIOS при старте

Гаджеты

LG WEB OS: приложения, обновления, настройка, проблемы со звуком

Гаджеты

.

Digital2.ru - Тренды, IT, WEB- разработка, Цифровая экономика
Свободное копирование и распространение материалов с сайта Digital2.ru
разрешено только с указанием активной ссылки на Digital2 как на источник.
Данный сайт в ходит структуру медиа группы: Online Payments Group Intellect Organic
Товарный знак: OPGIO
Copyright 2019 © All rights reserved

Connect
Подпишись на нас