Многопоточность в Python с примером: Изучите GIL в Python
Что такое нить?
Поток — это единица выполнения параллельного программирования. Многопоточность — это метод, который позволяет процессору одновременно выполнять множество задач одного процесса. Эти потоки могут выполняться индивидуально, совместно используя свои ресурсы процесса.
Что такое процесс?
Процесс — это, по сути, исполняемая программа. Когда вы запускаете приложение на своем компьютере (например, браузер или текстовый редактор), операционная система создает процесса.
Что такое многопоточность Python?
Многопоточность в Python программирование — это хорошо известный метод, при котором несколько потоков в процессе делят свое пространство данных с основным потоком, что делает обмен информацией и взаимодействие внутри потоков простым и эффективным. Потоки легче процессов. Несколько потоков могут выполняться индивидуально, совместно используя ресурсы процесса. Целью многопоточности является одновременное выполнение нескольких задач и функциональных ячеек.
Что такое многопроцессорность?
многопроцессорная обработка позволяет запускать несколько несвязанных процессов одновременно. Эти процессы не делят свои ресурсы и взаимодействуют через IPC.
Python Многопоточность против многопроцессорности
Чтобы понять процессы и потоки, рассмотрим следующий сценарий: EXE-файл на вашем компьютере представляет собой программу. Когда вы открываете его, ОС загружает его в память, а процессор выполняет. Экземпляр программы, который сейчас выполняется, называется процессом.
Каждый процесс будет иметь 2 фундаментальных компонента:
- Кодекс
- Данные
Теперь процесс может содержать одну или несколько подчастей, называемых потоки. Это зависит от архитектуры ОС. Вы можете рассматривать поток как часть процесса, который может выполняться операционной системой отдельно.
Другими словами, это поток инструкций, который может выполняться ОС независимо. Потоки внутри одного процесса совместно используют данные этого процесса и предназначены для совместной работы для обеспечения параллелизма.
Зачем использовать многопоточность?
Многопоточность позволяет разбить приложение на несколько подзадач и выполнять эти задачи одновременно. Если вы правильно используете многопоточность, скорость, производительность и рендеринг вашего приложения могут быть улучшены.
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, чтобы запустить программу. Если все было сделано правильно, вот что вы должны увидеть:
В следующих разделах вы узнаете больше об условиях гонки и о том, как с ними справиться.
КОД ОБЪЯСНЕНИЕ
- Эти операторы импортируют модуль времени и потока, которые используются для управления выполнением и задержкой процесса. Python потоки.
- Здесь вы определили функцию под названием поток_тест, который будет называться start_new_thread метод. Функция выполняет цикл while для четырех итераций и печатает имя потока, вызвавшего ее. После завершения итерации он печатает сообщение о том, что поток завершил выполнение.
- Это основной раздел вашей программы. Здесь вы просто вызываете start_new_thread метод с thread_test функция в качестве аргумента. Это создаст новый поток для функции, которую вы передаете в качестве аргумента, и начнет ее выполнение. Обратите внимание, что вы можете заменить это (тема_test) с любой другой функцией, которую вы хотите запустить как поток.
Модуль потоковой обработки
Этот модуль представляет собой высокоуровневую реализацию многопоточности в Python и фактический стандарт для управления многопоточными приложениями. Он обеспечивает широкий спектр функций по сравнению с модулем потока.

Вот список некоторых полезных функций, определенных в этом модуле:
Имя функции | Описание |
---|---|
activeCount () | Возвращает количество Нить объекты, которые все еще живы |
currentThread () | Возвращает текущий объект класса Thread. |
перечислить () | Перечисляет все активные объекты Thread. |
isDaemon() | Возвращает true, если поток является демоном. |
жив() | Возвращает true, если поток все еще жив. |
Методы класса потока | |
Начало() | Запускает активность потока. Его необходимо вызывать только один раз для каждого потока, поскольку при многократном вызове он выдаст ошибку времени выполнения. |
запустить() | Этот метод обозначает активность потока и может быть переопределен классом, расширяющим класс Thread. |
присоединиться() | Он блокирует выполнение другого кода до тех пор, пока поток, в котором был вызван метод join(), не будет завершен. |
Предыстория: Класс 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()
Это будет результат выполнения приведенного выше кода:
КОД ОБЪЯСНЕНИЕ
- Эта часть такая же, как и в нашем предыдущем примере. Здесь вы импортируете модуль времени и потока, которые используются для обработки выполнения и задержек 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Проблема Соферса.
Постановка проблемы для обедающих философов такова:
Пять философов сидят на круглом столе с пятью тарелками спагетти (разновидность макарон) и пятью вилками, как показано на схеме.

В любой момент времени философ должен либо есть, либо думать.
Более того, философ должен взять две вилки, стоящие рядом с ним (т. е. левую и правую вилки), прежде чем он сможет съесть спагетти. Проблема тупика возникает, когда все пять философов берут свои правые вилки одновременно.
Поскольку у каждого из философов есть одна вилка, они все будут ждать, пока остальные оставят вилку. В результате никто из них не сможет есть спагетти.
Аналогично, в параллельной системе тупик возникает, когда разные потоки или процессы (философы) пытаются одновременно получить общие системные ресурсы (вилки). В результате ни один из процессов не получает возможности выполниться, поскольку они ожидают другого ресурса, удерживаемого каким-либо другим процессом.
Условия гонки
Состояние гонки — это нежелательное состояние программы, которое возникает, когда система выполняет две или более операций одновременно. Например, рассмотрим этот простой цикл 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(). Здесь функция печати вызывается только после получения блокировки. Таким образом, выходные данные отображаются по одному и построчно.
Помимо блокировок, Python также поддерживает некоторые другие механизмы синхронизации потоков, перечисленные ниже:
- RЗамки
- Semaphores
- Conditions
- События и
- Барьеры
Глобальная блокировка интерпретатора (и как с ней бороться)
Прежде чем углубляться в детали GIL Python, давайте определим несколько терминов, которые будут полезны для понимания следующего раздела:
- Код, привязанный к ЦП: это относится к любому фрагменту кода, который будет напрямую выполняться ЦП.
- Код, связанный с вводом-выводом: это может быть любой код, который обращается к файловой системе через ОС.
- CPython: это ссылка реализация of Python и может быть описан как интерпретатор, написанный на C и Python (язык программирования).
Что такое GIL Python?
Глобальная блокировка переводчика (GIL) в Python — это блокировка процесса или мьютекс, используемый при работе с процессами. Он гарантирует, что один поток может получить доступ к определенному ресурсу одновременно, а также предотвращает одновременное использование объектов и байт-кодов. Это дает преимущества однопоточным программам в плане повышения производительности. 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, выполняющих тяжелые операции с загрузкой ЦП (фактически делая их однопоточными). Если вы хотите использовать в своем приложении несколько ядер ЦП, используйте команду многопроцессорная обработка модуль вместо этого.
Резюме
- Python поддерживает 2 модуля для многопоточности:
- __нить модуль: он обеспечивает низкоуровневую реализацию многопоточности и устарел.
- модуль потоковой передачи: обеспечивает высокоуровневую реализацию многопоточности и является текущим стандартом.
- Чтобы создать поток с помощью модуля потоков, необходимо выполнить следующие действия:
- Создайте класс, который расширяет Нить класса.
- Переопределить его конструктор (__init__).
- Отменить его запустить() метод.
- Создайте объект этого класса.
- Поток можно выполнить, вызвав метод Начало() метод.
- Команда присоединиться() метод можно использовать для блокировки других потоков до тех пор, пока этот поток (тот, в котором было вызвано соединение) не завершит выполнение.
- Состояние гонки возникает, когда несколько потоков одновременно обращаются к общему ресурсу или изменяют его.
- Этого можно избежать, Syncхронизация тем.
- Python поддерживает 6 способов синхронизации потоков:
- Волосы
- RЗамки
- Semaphores
- Conditions
- События и
- Барьеры
- Блокировки позволяют только определенному потоку, получившему блокировку, войти в критическую секцию.
- Блокировка имеет 2 основных метода:
- приобретать(): устанавливает состояние блокировки заперта. Если вызывается заблокированный объект, он блокируется до тех пор, пока ресурс не освободится.
- релиз(): устанавливает состояние блокировки разблокирован и возвращается. Если вызывается для разблокированного объекта, он возвращает false.
- Глобальная блокировка интерпретатора — это механизм, с помощью которого только 1 CPython Процесс интерпретатора может выполняться одновременно.
- Он использовался для облегчения функциональности подсчета ссылок в C.Pythonсборщик мусора s.
- Чтобы Python Для приложений с большими нагрузками на процессор следует использовать модуль многопроцессорности.