Многопоточность в Python с примером: Изучите GIL в Python
⚡ Умное резюме
Многопоточность в Python Этот метод запускает несколько потоков внутри одного процесса, так что они совместно используют память и работают одновременно. Модуль потоков создает и управляет этими потоками, а глобальная блокировка интерпретатора ограничивает истинный параллелизм, что делает этот метод наилучшим для задач, ограниченных вводом/выводом.
Python Язык программирования позволяет использовать многопроцессорность или многопоточность. В этом уроке вы узнаете, как писать многопоточные приложения на языке Python.
Что такое нить?
Поток — это единица выполнения в параллельном программировании. Многопоточность — это метод, позволяющий процессору выполнять множество задач одного процесса одновременно. Эти потоки могут выполняться по отдельности, совместно используя ресурсы процесса.
Что такое процесс?
Процесс — это, по сути, исполняемая программа. Когда вы запускаете приложение на своем компьютере (например, браузер или текстовый редактор), операционная система создает процесс. процесса.
Что такое многопоточность Python?
Многопоточность в Python Многопоточность — это хорошо известная техника, при которой несколько потоков в процессе совместно используют своё информационное пространство с основным потоком, что делает обмен информацией и взаимодействие внутри потоков простым и эффективным. Потоки легче процессов. Несколько потоков могут выполняться индивидуально, совместно используя свои ресурсы. Цель многопоточности — одновременное выполнение нескольких задач и функций.
Что такое многопроцессорность?
многопроцессорная обработка позволяет запускать несколько несвязанных процессов одновременно. Эти процессы не делят свои ресурсы и взаимодействуют через IPC.
Python Многопоточность против многопроцессорности
Чтобы понять, что такое процессы и потоки, рассмотрим следующий сценарий: файл .exe на вашем компьютере — это программа. Когда вы его открываете, операционная система загружает его в память, а процессор выполняет его. Экземпляр программы, который в данный момент запущен, называется процессом.
Каждый процесс имеет два основных компонента:
- Code
- Данные
Теперь процесс может содержать одну или несколько подчастей, называемых потоки. Это зависит от архитектуры операционной системы. Поток можно рассматривать как часть процесса, которая может выполняться операционной системой отдельно.
Иными словами, это поток инструкций, которые могут выполняться операционной системой независимо друг от друга. Потоки внутри одного процесса совместно используют данные этого процесса и предназначены для совместной работы, что обеспечивает параллельную обработку.
Зачем использовать многопоточность?
Многопоточность позволяет разбить приложение на несколько подзадач и выполнять эти задачи одновременно. Если вы правильно используете многопоточность, скорость, производительность и рендеринг вашего приложения могут быть улучшены.
Python многопоточность
Python Поддерживаются конструкции как для многопроцессорной, так и для многопоточной обработки. В этом руководстве вы сосредоточитесь в основном на реализации многопоточный приложения с PythonДля работы с потоками можно использовать два основных модуля. Python:
- нить модуль и
- нарезания резьбы модуль
Однако в PythonСуществует также так называемая глобальная блокировка интерпретатора (GIL). Она не обеспечивает существенного повышения производительности и может даже привести к её снижению. уменьшить расходы производительность некоторых многопоточных приложений. Вы узнаете все об этом в следующих разделах этого урока.
Модули Thread и Threading
Два модуля, о которых вы узнаете в этом уроке, — это модуль потока и модуль потоковой передачи.
Однако модуль thread уже давно устарел. Начиная с Python 3, он был обозначен как устаревший и доступен только как _нить для обратной совместимости.
Вам следует использовать более высокий уровень нарезания резьбы Модуль для приложений, которые вы планируете развернуть. Модуль потоков здесь рассмотрен только в образовательных целях.
Модуль потока
Синтаксис создания нового потока с использованием этого модуля следующий:
thread.start_new_thread(function_name, arguments)
Хорошо, теперь вы изучили основную теорию и приступили к программированию. Итак, откройте свой IDLE или блокнот и введите следующее:
import time import _thread def thread_test(name, wait): i = 0 while i <= 3: time.sleep(wait) print("Running %s\n" %name) i = i + 1 print("%s has finished execution" %name) if __name__ == "__main__": _thread.start_new_thread(thread_test, ("First Thread", 1)) _thread.start_new_thread(thread_test, ("Second Thread", 2)) _thread.start_new_thread(thread_test, ("Third Thread", 3))
Сохраните файл и нажмите F5, чтобы запустить программу. Если все было сделано правильно, вот что вы должны увидеть:
В следующих разделах вы узнаете больше об условиях гонки и о том, как с ними справляться.
КОД ОБЪЯСНЕНИЕ
- Эти операторы импортируют модули time и thread, которые используются для управления выполнением и задержкой. Python потоки.
- Здесь вы определили функцию под названием поток_тест, который будет называться start_new_thread Метод. Функция выполняет цикл while в течение четырех итераций и выводит имя потока, который ее вызвал. После завершения итерации она выводит сообщение о том, что поток завершил выполнение.
- Это основной раздел вашей программы. Здесь вы просто вызываете start_new_thread метод с thread_test Функция может быть передана в качестве аргумента. Это создаст новый поток для функции, которую вы передали в качестве аргумента, и запустит её выполнение. Обратите внимание, что вы можете заменить (thread_test) любой другой функцией, которую хотите запустить в отдельном потоке.
Модуль потоковой обработки
Этот модуль представляет собой высокоуровневую реализацию многопоточности в Python и фактический стандарт управления многопоточными приложениями. Он обеспечивает широкий спектр функций по сравнению с модулем потока.
Структура модуля Threading
Вот список некоторых полезных функций, определенных в этом модуле:
| Имя функции | Описание |
|---|---|
| activeCount () | Возвращает количество Нить объекты, которые еще живы. |
| currentThread () | Возвращает текущий объект класса Thread. |
| перечислить () | Перечисляет все активные объекты Thread. |
| isDaemon() | Возвращает true, если поток является демоном. |
| жив() | Возвращает true, если поток все еще жив. |
| Методы класса потока | |
| Начало() | Запускает активность потока. Его необходимо вызывать только один раз для каждого потока, поскольку при многократном вызове он выдаст ошибку времени выполнения. |
| запустить() | Этот метод обозначает активность потока и может быть переопределен классом, расширяющим класс Thread. |
| присоединиться() | Он блокирует выполнение другого кода до тех пор, пока поток, в котором был вызван метод join(), не будет завершен. |
Предыстория: Класс Thread
Прежде чем начать писать многопоточные программы с использованием модуля threading, крайне важно понять класс Thread. Класс Thread — это основной класс, определяющий шаблон и операции потока. Python.
Самый распространенный способ создания многопоточного Python Задача приложения — объявить класс, который наследует класс Thread и переопределяет его метод run().
Вкратце, класс Thread означает последовательность кода, которая выполняется в отдельном нить контроля.
Итак, при написании многопоточного приложения вам предстоит сделать следующее:
- Определите класс, который наследует класс Thread.
- Переопределить __init__ конструктор
- Переопределить запустить() метод
После создания объекта потока Начало() Этот метод можно использовать для начала выполнения данной операции, и присоединиться() метод можно использовать для блокировки всего остального кода до завершения текущего действия.
Теперь давайте попробуем использовать модуль потоков для реализации вашего предыдущего примера. Снова запустите ваш IDLE и введите следующее:
import time import threading class threadtester (threading.Thread): def __init__(self, id, name, i): threading.Thread.__init__(self) self.id = id self.name = name self.i = i def run(self): thread_test(self.name, self.i, 5) print ("%s has finished execution " %self.name) def thread_test(name, wait, i): while i: time.sleep(wait) print ("Running %s \n" %name) i = i - 1 if __name__=="__main__": thread1 = threadtester(1, "First Thread", 1) thread2 = threadtester(2, "Second Thread", 2) thread3 = threadtester(3, "Third Thread", 3) thread1.start() thread2.start() thread3.start() thread1.join() thread2.join() thread3.join()
Это будет результат выполнения приведенного выше кода:
КОД ОБЪЯСНЕНИЕ
- Эта часть аналогична нашему предыдущему примеру. Здесь вы импортируете модули time и thread, которые используются для управления выполнением и задержками. Python потоки.
- В этом разделе вы создаете класс под названием threadtester, который наследует или расширяет Нить класс модуля потоковой обработки. Это один из наиболее распространенных способов создания потоков в Python. Однако вам следует переопределить только конструктор и запустить() метод в вашем приложении. Как вы можете видеть в приведенном выше примере кода, __init__ метод (конструктор) был переопределен. Аналогично, вы также переопределили запустить() метод. Он содержит код, который вы хотите выполнить внутри потока. В этом примере вы вызвали функцию thread_test().
- Это метод thread_test(), который принимает значение i В качестве аргумента функция уменьшает значение на 1 на каждой итерации и проходит по остальной части кода, пока i не станет равным 0. На каждой итерации она выводит имя текущего выполняющегося потока и приостанавливает выполнение на определенное количество секунд (которое также принимается в качестве аргумента).
- thread1 = threadtester(1, «Первый поток», 1) Здесь мы создаем поток и передаем три параметра, которые мы объявили в __init__. Первый параметр — это идентификатор потока, второй параметр — имя потока, а третий параметр — счетчик, который определяет, сколько раз должен выполняться цикл while.
- thread2.start() Метод start используется для запуска выполнения потока. Внутри функция start() вызывает метод run() вашего класса.
- thread3.join() Метод join() блокирует выполнение другого кода и ожидает завершения потока, в котором он был вызван.
Как вы уже знаете, потоки, находящиеся в одном процессе, имеют доступ к памяти и данным этого процесса. В результате, если несколько потоков пытаются одновременно изменять данные или получать к ним доступ, могут возникнуть ошибки.
В следующем разделе вы увидите различные виды осложнений, которые могут возникнуть, когда потоки обращаются к данным и критической секции без проверки наличия существующих транзакций доступа.
Тупики и состояния гонки
Прежде чем изучать взаимоблокировки и состояния гонки, полезно будет понять несколько основных определений, связанных с параллельным программированием:
- Критический раздел: Это фрагмент кода, который обращается к общим переменным или изменяет их, и его выполнение должно быть атомарной транзакцией.
- Переключение контекста: Это процесс, который выполняет центральный процессор для сохранения состояния потока перед переходом от одной задачи к другой, чтобы впоследствии можно было возобновить выполнение с того же места.
Тупики
Тупики являются наиболее опасной проблемой, с которой сталкиваются разработчики при написании параллельных/многопоточных приложений на PythonЛучший способ понять взаимоблокировки — это использовать классический пример из области информатики, известный как... Где пообедать PhiloПроблема Соферса.
Постановка проблемы для обедающих философов такова:
На круглом столе сидят пять философов, перед ними пять тарелок со спагетти (разновидность макаронных изделий) и пять вилок, как показано на схеме.
Где пообедать PhiloПроблема Соферса
В любой момент времени философ должен либо есть, либо думать.
Более того, философ должен взять две вилки, стоящие рядом с ним (т. е. левую и правую вилки), прежде чем он сможет съесть спагетти. Проблема тупика возникает, когда все пять философов берут свои правые вилки одновременно.
Поскольку у каждого из философов есть одна вилка, они все будут ждать, пока остальные оставят вилку. В результате никто из них не сможет есть спагетти.
Аналогично, в параллельной системе тупик возникает, когда разные потоки или процессы (философы) пытаются одновременно получить общие системные ресурсы (вилки). В результате ни один из процессов не получает возможности выполниться, поскольку они ожидают другого ресурса, удерживаемого каким-либо другим процессом.
Условия гонки
Состояние гонки — это нежелательное состояние программы, возникающее, когда система выполняет две или более операций одновременно. Например, рассмотрим этот простой цикл for:
i=0; # a global variable for x in range(100): print(i) i+=1;
Если вы создадите n Если одновременно выполняется множество потоков, невозможно определить значение переменной i (которая используется всеми потоками) после завершения выполнения программы. Это связано с тем, что в реальной многопоточной среде потоки могут перекрываться, и значение переменной i, полученное и измененное одним потоком, может измениться в процессе обращения к ней другого потока.
Это два основных класса проблем, которые могут возникнуть в многопоточной или распределенной среде. Python Приложение. В следующем разделе вы узнаете, как решить эту проблему путем синхронизации потоков.
Syncхронизация тем
Для решения проблем с состояниями гонки, взаимоблокировками и другими проблемами, связанными с потоками, модуль потоковой передачи предоставляет Замка объект. Идея состоит в том, что когда поток хочет получить доступ к определенному ресурсу, он блокирует этот ресурс. Как только поток блокирует определенный ресурс, ни один другой поток не сможет получить к нему доступ, пока блокировка не будет снята. В результате изменения ресурса будут атомарными, а условия гонки будут предотвращены.
Блокировка — это примитив синхронизации низкого уровня, реализуемый _нить модуль. В любой момент времени блокировка может находиться в одном из двух состояний: запертый or разблокирован. Он поддерживает два метода:
- приобретать(): Когда состояние блокировки разблокировано, вызов метода acquire() изменит состояние на заблокированное и вернет управление. Однако, если состояние заблокировано, вызов acquire() блокируется до тех пор, пока другой поток не вызовет метод release().
- выпускать(): Метод Release() используется для установки состояния разблокировки, т. е. для снятия блокировки. Его может вызвать любой поток, не обязательно тот, который получил блокировку.
Вот пример использования блокировок в ваших приложениях. Запустите ваше приложение. IDLE и введите следующее:
import threading lock = threading.Lock() def first_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the first funcion') lock.release() def second_function(): for i in range(5): lock.acquire() print ('lock acquired') print ('Executing the second funcion') lock.release() if __name__=="__main__": thread_one = threading.Thread(target=first_function) thread_two = threading.Thread(target=second_function) thread_one.start() thread_two.start() thread_one.join() thread_two.join()
Теперь нажмите F5. Вы должны увидеть такой вывод:
КОД ОБЪЯСНЕНИЕ
- Здесь вы просто создаете новую блокировку, вызывая threading.Lock () заводская функция. Внутри Lock() возвращает экземпляр наиболее эффективного конкретного класса Lock, поддерживаемого платформой.
- В первом операторе вы получаете блокировку, вызывая методacquire(). Когда блокировка предоставлена, вы печатаете «блокировка получена» на консоль. Как только весь код, который вы хотите, чтобы поток выполнил, завершил выполнение, вы снимаете блокировку, вызывая метод Release().
Теория хороша, но как узнать, действительно ли блокировка сработала? Если вы посмотрите на вывод, то увидите, что каждое из операторов print печатает ровно одну строку за раз. Вспомните, что в предыдущем примере вывод print был случайным, потому что несколько потоков одновременно обращались к методу print(). Здесь же функция print вызывается только после получения блокировки. Поэтому вывод отображается по одному, построчно.
Кроме замков, Python Также поддерживаются некоторые другие механизмы для обработки синхронизации потоков, перечисленные ниже:
- RЗамки
- Semaphores
- Conditions
- События и
- Барьеры
Глобальная блокировка интерпретатора (и как с ней бороться)
Прежде чем вдаваться в подробности PythonВ рамках GIL давайте определим несколько терминов, которые будут полезны для понимания следующего раздела:
- Код, ресурсоемкий для ЦП: это любой фрагмент кода, который будет непосредственно выполняться ЦП.
- Код, связанный с операциями ввода-вывода: это может быть любой код, который обращается к файловой системе через операционную систему.
- CPython: это ссылка реализация of Python и может быть описан как интерпретатор, написанный на C и Python (язык программирования).
Что такое GIL Python?
Глобальная блокировка переводчика (GIL) in Python GIL — это блокировка процесса или мьютекс, используемый при работе с процессами. Он гарантирует, что только один поток может получить доступ к определенному ресурсу за раз, а также предотвращает одновременное использование объектов и байт-кода. Это повышает производительность однопоточных программ. GIL в Python очень просто и легко реализовать.
Блокировку можно использовать, чтобы гарантировать, что только один поток имеет доступ к определенному ресурсу в определенный момент времени.
Одна из особенностей Python заключается в том, что он использует глобальную блокировку для каждого процесса интерпретатора, а это означает, что каждый процесс обрабатывает Python сам интерпретатор как ресурс.
Например, предположим, что вы написали Python Программа, использующая два потока для выполнения как операций ЦП, так и операций ввода-вывода. При запуске этой программы происходит следующее:
- Python Интерпретатор создает новый процесс и запускает потоки.
- Когда поток-1 запускается, он сначала получает GIL и блокирует его.
- Если поток-2 хочет выполниться сейчас, ему придется дождаться освобождения GIL, даже если другой процессор свободен.
- Теперь предположим, что поток-1 ожидает операции ввода-вывода. В это время он выпустит GIL, и поток-2 получит его.
- Если после завершения операций ввода-вывода поток-1 хочет выполниться сейчас, ему снова придется дождаться освобождения GIL потоком-2.
Благодаря этому только один поток может получить доступ к интерпретатору в любой момент времени, а это означает, что будет выполняться только один поток. Python код в определенный момент времени.
В одноядерном процессоре это допустимо, поскольку он использует метод разделения времени (см. первый раздел этого руководства) для обработки потоков. Однако в случае многоядерных процессоров функция, сильно зависящая от ЦП и выполняющаяся в нескольких потоках, окажет значительное влияние на эффективность программы, поскольку она фактически не будет использовать все доступные ядра одновременно.
Зачем нужен был GIL?
СPython Сборщик мусора использует эффективный метод управления памятью, известный как подсчет ссылок. Вот как это работает: каждый объект в Python Объект имеет счетчик ссылок, который увеличивается при присвоении ему нового имени переменной или добавлении в контейнер (например, кортежи, списки и т. д.). Аналогично, счетчик ссылок уменьшается, когда ссылка выходит из области видимости или когда вызывается оператор del. Когда счетчик ссылок объекта достигает 0, он удаляется сборщиком мусора, и выделенная память освобождается.
Но проблема в том, что переменная счетчика ссылок, как и любая другая глобальная переменная, подвержена состояниям гонки. Чтобы решить эту проблему, разработчики Python Было решено использовать глобальную блокировку интерпретатора. Другой вариант заключался в добавлении блокировки к каждому объекту, что привело бы к взаимоблокировкам и увеличению накладных расходов на вызовы acquire() и release().
Таким образом, GIL является существенным ограничением для многопоточной обработки. Python Программы, выполняющие ресурсоемкие операции, сильно нагружающие процессор (фактически однопоточные). Если вы хотите использовать несколько ядер процессора в своем приложении, используйте... многопроцессорная обработка модуль вместо этого.









