Потоки Python: введение

Опубликовано: 2022-10-25

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

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

Давайте начнем.

Процессы и потоки: в чем разница?

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

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

Это может быть что угодно — от скрипта Python или веб-браузера, такого как Chrome, до приложения для видеоконференций. Если вы запустите Диспетчер задач на своем компьютере и перейдете в «Производительность» -> « ЦП », вы сможете увидеть процессы и потоки, которые в настоящее время выполняются на ядрах вашего ЦП.

cpu-proc-потоки

Понимание процессов и потоков

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

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

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

процесс-и-потоки

В ЦП с N ядрами N процессов могут выполняться параллельно в одно и то же время. Однако два потока одного и того же процесса никогда не могут выполняться параллельно, но могут выполняться одновременно. Мы рассмотрим концепцию параллелизма и параллелизма в следующем разделе.

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

Особенность Процесс Нить
Память Выделенная память Общая память
Режим исполнения Параллельно, параллельно Параллельно; но не параллельно
Исполнением занимается Операционная система Интерпретатор CPython

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

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

Например, рассмотрим два потока t1 и t2 одного и того же процесса. Поскольку потоки совместно используют одни и те же данные, когда t1 считывает конкретное значение k , t2 может изменить то же самое значение k . Это может привести к взаимоблокировкам и нежелательным результатам. Но только один из потоков может получить блокировку и работать в любой момент. Поэтому GIL также обеспечивает потокобезопасность .

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

Параллелизм против параллелизма: обзор

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

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

многоядерный параллелизм

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

код

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

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

Если вы хотите реализовать параллелизм на уровне процессов в своем приложении, рассмотрите возможность использования многопроцессорности.

Модуль Python Threading: первые шаги

Python поставляется с модулем threading , который можно импортировать в сценарий Python.

 import threading

Чтобы создать объект потока в Python, вы можете использовать конструктор Thread : threading.Thread(...) . Это общий синтаксис, которого достаточно для большинства реализаций потоков:

 threading.Thread(target=...,args=...)

Здесь,

  • target — аргумент ключевого слова, обозначающий вызываемый объект Python.
  • args — это кортеж аргументов, которые принимает цель.

Вам понадобится Python 3.x для запуска примеров кода в этом руководстве. Загрузите код и следуйте инструкциям.

Как определить и запустить потоки в Python

Давайте определим поток, который запускает целевую функцию.

Целевая функция — some_func .

 import threading import time def some_func(): print("Running some_func...") time.sleep(2) print("Finished running some_func.") thread1 = threading.Thread(target=some_func) thread1.start() print(threading.active_count())

Давайте разберем, что делает приведенный выше фрагмент кода:

  • Он импортирует модули threading и time .
  • Функция some_func имеет описательные операторы print() и включает операцию приостановки на две секунды: time.sleep(n) заставляет функцию приостанавливаться на n секунд.
  • Затем мы определяем поток thread_1 с целью как some_func . threading.Thread(target=...) создает объект потока.
  • Примечание . Укажите имя функции, а не вызов функции; используйте some_func а не some_func() .
  • Создание объекта потока не запускает поток; вызывает метод start() для объекта потока.
  • Чтобы получить количество активных потоков, мы используем active_count() .

Сценарий Python выполняется в основном потоке, и мы создаем еще один поток ( thread1 ) для запуска функции some_func , поэтому количество активных потоков равно двум, как видно из вывода:

 # Output Running some_func... 2 Finished running some_func.

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

thread1-ex

Ожидание завершения выполнения потоков

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

 import threading import time def some_func(): print("Running some_func...") time.sleep(2) print("Finished running some_func.") thread1 = threading.Thread(target=some_func) thread1.start() thread1.join() print(threading.active_count())

Теперь thread1 завершил выполнение до того, как мы распечатаем количество активных потоков. Таким образом, работает только основной поток, что означает, что количество активных потоков равно единице.

 # Output Running some_func... Finished running some_func. 1

Как запустить несколько потоков в Python

Далее создадим два потока для запуска двух разных функций.

Здесь count_down — это функция, которая принимает число в качестве аргумента и ведет обратный отсчет от этого числа до нуля.

 def count_down(n): for i in range(n,-1,-1): print(i)

Мы определяем count_up , еще одну функцию Python, которая считает от нуля до заданного числа.

 def count_up(n): for i in range(n+1): print(i)

При использовании функции range() с синтаксисом range(start, stop, step) stop в конечной точке по умолчанию исключается.

– Для обратного отсчета от определенного числа до нуля можно использовать отрицательное значение step , равное -1, и установить значение stop на -1, чтобы включить ноль.

- Точно так же, чтобы сосчитать до n , вы должны установить значение stop на n + 1 . Поскольку значения start и step по умолчанию равны 0 и 1 соответственно, вы можете использовать range(n + 1) для получения последовательности от 0 до n.

Затем мы определяем два потока, thread1 и thread2 , для запуска функций count_down и count_up соответственно. Мы добавляем операторы print и операции sleep для обеих функций.

При создании объектов потока обратите внимание, что аргументы целевой функции должны быть указаны в виде кортежа — в параметре args . Поскольку обе функции ( count_down и count_up ) принимают один аргумент. Вам нужно будет явно вставить запятую после значения. Это гарантирует, что аргумент по-прежнему передается как кортеж, поскольку последующие элементы выводятся как None .

 import threading import time def count_down(n): for i in range(n,-1,-1): print("Running thread1....") print(i) time.sleep(1) def count_up(n): for i in range(n+1): print("Running thread2...") print(i) time.sleep(1) thread1 = threading.Thread(target=count_down,args=(10,)) thread2 = threading.Thread(target=count_up,args=(5,)) thread1.start() thread2.start()

В выводе:

  • Функция count_up работает в thread2 и считает до 5, начиная с 0.
  • Функция count_down работает в thread1 1, считая от 10 до 0.
 # Output Running thread1.... 10 Running thread2... 0 Running thread1.... 9 Running thread2... 1 Running thread1.... 8 Running thread2... 2 Running thread1.... 7 Running thread2... 3 Running thread1.... 6 Running thread2... 4 Running thread1.... 5 Running thread2... 5 Running thread1.... 4 Running thread1.... 3 Running thread1.... 2 Running thread1.... 1 Running thread1.... 0

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

Подводя итоги

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

  • Конструктор Thread можно использовать для создания объекта потока. Использование threading.Thread(target=<callable>,args=(<tuple of args>)) создает поток, который запускает целевой вызываемый объект с аргументами, указанными в args .
  • Программа Python выполняется в основном потоке, поэтому создаваемые вами объекты потока являются дополнительными потоками. Вы можете вызвать функцию active_count() , которая возвращает количество активных потоков в любом случае.
  • Вы можете запустить поток, используя метод start() для объекта потока, и дождаться завершения его выполнения с помощью метода join() .

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