Многопоточность в Python с примером глобальной блокировки интерпретатора (GIL)

Содержание:

Anonim

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

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

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

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

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

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

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

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

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

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

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

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

  • Код
  • Данные

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

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

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

  • Что такое поток?
  • Что такое процесс?
  • Что такое многопоточность?
  • Что такое многопроцессорность?
  • Многопоточность Python против многопроцессорности
  • Зачем использовать многопоточность?
  • Многопоточность Python
  • Модули Thread и Threading
  • Модуль потока
  • Модуль Threading
  • Тупики и условия гонки
  • Синхронизация потоков
  • Что такое ГИЛ?
  • Зачем был нужен GIL?

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

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

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

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

  1. Модуль потока и
  2. Модуль потоковой передачи

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

Модули Thread и Threading

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

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

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

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

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

thread.start_new_thread(function_name, arguments)

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

import timeimport _threaddef thread_test(name, wait):i = 0while i <= 3:time.sleep(wait)print("Running %s\n" %name)i = i + 1print("%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) любой другой функцией, которую хотите запустить как поток.

Модуль Threading

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

Структура модуля Threading

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

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

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

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

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

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

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

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

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

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

import timeimport threadingclass threadtester (threading.Thread):def __init__(self, id, name, i):threading.Thread.__init__(self)self.id = idself.name = nameself.i = idef 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 - 1if __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 используется для запуска выполнения потока. Внутренне функция start () вызывает метод run () вашего класса.

  6. thread3.join ()

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

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

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

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

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

  • Критический раздел

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

  • Переключение контекста

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

Тупиковые ситуации

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

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

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

Проблема обедающих философов

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

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

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

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

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

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

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

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

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

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

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

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

  1. приобретать()

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

  2. релиз()

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

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

import threadinglock = 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. В первом операторе вы получаете блокировку, вызывая метод acqu (). Когда блокировка предоставлена, вы выводите на консоль «блокировка получена» . Как только весь код, который вы хотите запустить в потоке, завершит выполнение, вы снимаете блокировку, вызывая метод release ().

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

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

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

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

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

  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 хочет выполнить сейчас, ему снова придется ждать, пока поток-2 не выпустит GIL.

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

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

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

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

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

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

Резюме

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