Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Industrial programming >> Python

Многопоточность в Python на примере:изучаем GIL в Python

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

Что такое поток?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. тред модуль и
  2. потоки модуль

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

Модули Thread и Threading

В этом руководстве вы узнаете о двух модулях:модуль потока. и модуль потоков .

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

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

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

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

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, чтобы запустить программу. Если все было сделано правильно, вы должны увидеть следующий вывод:

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

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

  1. Эти операторы импортируют модуль времени и потока, которые используются для управления выполнением и задержкой потоков Python.
  2. Здесь вы определили функцию с именем thread_test. который будет вызываться start_new_thread метод. Функция выполняет цикл while для четырех итераций и печатает имя потока, который ее вызвал. После завершения итерации выводится сообщение о том, что выполнение потока завершено.
  3. Это основной раздел вашей программы. Здесь вы просто вызываете start_new_thread метод с thread_test функция в качестве аргумента. Это создаст новый поток для функции, которую вы передаете в качестве аргумента, и начнет ее выполнение. Обратите внимание, что вы можете заменить это (thread_ test) с любой другой функцией, которую вы хотите запустить как поток.

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

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

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

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

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

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

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

Короче говоря, класс Thread означает последовательность кода, которая выполняется в отдельном потоке. контроля.

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

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

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

Теперь давайте попробуем использовать модуль threading для реализации вашего предыдущего примера. Снова запустите 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()

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

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

  1. Эта часть аналогична предыдущему примеру. Здесь вы импортируете модуль времени и потока, которые используются для обработки выполнения и задержек потоков Python.
  2. В этом разделе вы создаете класс threadtester, который наследует или расширяет класс Thread. класс модуля потоков. Это один из самых распространенных способов создания потоков в Python. Однако вам следует переопределить только конструктор и функцию run(). метод в вашем приложении. Как видно из приведенного выше примера кода, __init__ метод (конструктор) был переопределен. Точно так же вы также переопределили run() метод. Он содержит код, который вы хотите выполнить внутри потока. В этом примере вы вызвали функцию thread_test().
  3. Это метод thread_test(), который принимает значение i в качестве аргумента, уменьшает его на 1 на каждой итерации и перебирает остальную часть кода до тех пор, пока i не станет 0. На каждой итерации он печатает имя текущего выполняющегося потока и приостанавливается на секунды ожидания (что также принимается в качестве аргумента ).
  4. thread1 =threadtester(1, «Первая нить», 1) Здесь мы создаем нить и передаем три параметра, которые мы объявили в __init__. Первый параметр — это идентификатор потока, второй параметр — это имя потока, а третий параметр — это счетчик, который определяет, сколько раз должен выполняться цикл while.
  5. thread2.start()Метод запуска используется для запуска выполнения потока. Внутри функция start() вызывает метод run() вашего класса.
  6. thread3.join() Метод join() блокирует выполнение другого кода и ожидает завершения потока, в котором он был вызван.

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

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

Взаимоблокировки и условия гонки

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

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

Взаимоблокировки

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

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

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

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

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

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

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

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

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

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

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

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

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

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

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

  1. получить() Когда состояние блокировки разблокировано, вызов метода Acquis() изменит состояние на заблокированное и возврат. Тем не менее, если состояние заблокировано, вызов Acquis() блокируется до тех пор, пока какой-либо другой поток не вызовет метод release().
  2. релиз() Метод 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. Вы должны увидеть такой вывод:

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

  1. Здесь вы просто создаете новую блокировку, вызывая метод threading.Lock(). заводская функция. Внутри Lock() возвращает экземпляр наиболее эффективного конкретного класса Lock, поддерживаемого платформой.
  2. В первом операторе вы получаете блокировку, вызывая метод Acquire(). Когда блокировка предоставлена, вы печатаете «блокировка получена» к консоли. После завершения выполнения всего кода, который вы хотите запустить в потоке, вы снимаете блокировку, вызывая метод release().

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

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

  1. RБлокировки
  2. Семафоры
  3. Условия
  4. События и
  5. Барьеры

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

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

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

Что такое GIL в Python?

Глобальная блокировка интерпретатора (GIL) в python — это блокировка процесса или мьютекс, используемый при работе с процессами. Это гарантирует, что один поток может получить доступ к определенному ресурсу за раз, а также предотвращает одновременное использование объектов и байт-кодов. Это приносит пользу однопоточным программам в повышении производительности. GIL в Python очень прост и легок в реализации.

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

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

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

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

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

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

Зачем был нужен GIL?

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

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

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

Обзор

  • Python поддерживает 2 модуля многопоточности:
    1. __поток модуль:обеспечивает низкоуровневую реализацию многопоточности и устарел.
    2. модуль многопоточности :обеспечивает высокоуровневую реализацию многопоточности и является текущим стандартом.
  • Чтобы создать цепочку с помощью модуля потоков, необходимо сделать следующее:
    1. Создайте класс, расширяющий Thread. класс.
    2. Переопределить его конструктор (__init__).
    3. Переопределить его run() метод.
    4. Создайте объект этого класса.
  • Поток можно запустить, вызвав метод start(). метод.
  • Функция join() можно использовать для блокировки других потоков до тех пор, пока этот поток (тот, для которого было вызвано объединение) не завершит выполнение.
  • Состояние гонки возникает, когда несколько потоков одновременно обращаются к общему ресурсу или изменяют его.
  • Этого можно избежать путем синхронизации потоков.
  • Python поддерживает 6 способов синхронизации потоков:
    1. Замки
    2. RБлокировки
    3. Семафоры
    4. Условия
    5. События и
    6. Барьеры
  • Блокировки позволяют только определенному потоку, получившему блокировку, войти в критическую секцию.
  • У блокировки есть 2 основных метода:
    1. получить() :устанавливает состояние блокировки на заблокировано. При вызове для заблокированного объекта он блокируется до тех пор, пока ресурс не освободится.
    2. релиз() :устанавливает состояние блокировки на разблокировано. и возвращается. При вызове незаблокированного объекта возвращается значение false.
  • Глобальная блокировка интерпретатора — это механизм, с помощью которого одновременно может выполняться только один процесс интерпретатора CPython.
  • Он использовался для облегчения функции подсчета ссылок сборщика мусора CPythons.
  • Чтобы создавать приложения Python с тяжелыми операциями, связанными с ЦП, следует использовать модуль многопроцессорности.

Python

  1. Функция free() в библиотеке C:как использовать? Учитесь на примере
  2. Функция Python String strip() с ПРИМЕРОМ
  3. Количество строк Python() с ПРИМЕРАМИ
  4. Функция Python round() с ПРИМЕРАМИ
  5. Функция Python map() с ПРИМЕРАМИ
  6. Python Timeit() с примерами
  7. Счетчик Python в коллекциях с примером
  8. Счетчик списка Python() с ПРИМЕРАМИ
  9. Индекс списка Python() с примером
  10. С# — многопоточность