В этой статье (перевод [1]) обсуждаются базовые вопросы реализации многопоточности (multithreading) на языке программирования Python. Точно так же, как многопроцессорность (multiprocessing), многопоточность это способ реализации многозадачности (multitasking). В многопоточности используется концепция потоков (threads). Давайте сначала разберемся в концепции потока архитектуры компьютеров.
[Что такое процесс в Python]
В компьютерных технологиях процесс это экземпляр выполняемой программы. В любом процессе есть 3 базовых компонента:
• Исполняемая программа. • Связанные с программой данные, которые ей необходимы (переменные, рабочее пространство, буферы и т. д.). • Контекст выполнения программы (состояние процесса, State).
[Введение в Python Threading]
Поток (thread) это сущность внутри процесса, которой планировщик операционной системы (ОС) может выделять процессорное время для выполнения. Также это самый малый юнит обработки, который может выполняться в ОС. Простыми словами поток это последовательность инструкций в программе, которая может выполняться независимо (почти) от другого кода. Для упрощения вы можете представить себе поток как подмножество процесса. Поток содержит всю информацию о своем контексте выполнения в блоке управления потоком (Thread Control Block, TCB):
• Идентификатор потока: Unique Thread ID (TID), назначаемый каждому новому потоку. • Указатель стека: указывает на блок памяти, где находится стек потока. Стек содержит локальные переменные, которые находятся в области видимости потока. • Счетчик инструкций программы: регистр, который хранит адрес текущей выполняемой потоком инструкции. • Состояние потока: это может быть состояние работы (running), готовность к работе (ready), ожидание (waiting), запуск (starting) или завершение (done). • Набор регистров потока: регистры, назначенные потоку для вычислений. • Указатель на родительский процесс: указатель на блок управления процессом (Process control block, PCB) процесса, в котором живет поток.
Рассмотрим следующую диаграмму, чтобы понять взаимосвязь между процессом и его потоком:
Рис. 1. Взаимоотношения между процессом (Process) и его потоком (Thread).
Несколько потоков могут существовать в одном процессе, где:
• Каждый поток содержит свой собственный набор регистров и локальные переменные (сохраненные в стеке). • Все потоки процесса используют общие глобальные переменные (сохраненные в куче) и коде программы.
Следующая диаграмма иллюстрирует присутствие нескольких потоков в памяти:
Рис. 2. Присутствие нескольких потоков в памяти.
[Потоки в Python]
Многопоточность определена как способность процессора выполнять несколько потоков конкурентно. На простом, одноядерном CPU это достигается быстрым переключением между потоками. Это называют термином "переключение контекста". Всякий раз, когда срабатывает какое-либо прерывание, вызывающее планировщик (это может быть прерывание по таймеру, прерывание от какого-либо события в системе, например операции ввода/вывода), происходит переключение контекста. При этом состояние работающего потока сохраняется в TCB, а состояние другого потока загружается из TCB), и потоки меняются ролями. Переключение контекста происходит так часто, что кажется, что они работают параллельно (это называется многозадачностью).
На диаграмме ниже показан процесс, содержащий два активных потока:
Рис. 3. Многопоточность.
Основные типы потоков в Python:
• Main Thread: начальный поток, запускающийся при старте программы. • Daemon Threads: фоновые потоки, которые автоматически завершаются, когда завершается main thread. • Non-Daemon Threads: потоки, которые продолжают работать, пока не завершат свою задачу, даже если main thread завершится.
Многопоточность Python. Модуль threading предоставляет очень простое и интуитивное API для порождения нескольких потоков в программе. Давайте попробуем по шагам разобрать многопоточный код.
Шаг 1. Импорт модуля.
import threading
Шаг 2. Создание потока. Для создания нового потока мы создаем объект класса Thread. Конструктор принимает параметры target и args. Параметр target это функция, выполняемая потоком, в то время как args это аргументы, передаваемые в целевую функцию потока.
Шаг 3. Запуск потока. Для запуска потока мы используем метод start() класса Thread.
t1.start()
t2.start()
Шаг 4. Завершение выполнения потока. Как только потоки запущены, текущая программа (вы можете думать об этом как о main thread) также продолжает выполнение. Чтобы остановить текущую программу до завершения потока, мы используем метод join().
t1.join()
t2.join()
В результате текущая программ будет сначала ждать завершения потока t1 и затем t2. Как только они завершат работу, выполнение текущей программы продолжится.
Давайте рассмотрим простой пример использования модуля threading.
Этот код демонстрирует, как использовать модуль threading для вычисления квадрата и куба одновременно. Создаются два потока, t1 и t2, для выполнения этих вычислений. Они запускаются, и их результаты печатаются параллельно перед печатью программы "Завершено!", когда оба потока завершатся. Многопоточность используется для достижения параллелизма и повышения производительности программ при решении ресурсоемких задач.
import threading
defprint_cube(num):
print("Cube: {}".format(num * num * num))
В этом примере мы используем функцию os.getpid() для получения ID текущего процесса. Мы используем функцию threading.main_thread() для получения объекта main thread. В нормальных условиях main thread это поток, их которого запускался интерпретатор Python. Атрибут name объекта потока используется для получения имени потока. Затем мы используем функцию threading.current_thread() для получения текущего объекта потока.
Рассмотрим приведенную ниже программу Python, в которой мы печатаем имя потока и соответствующий процесс для каждой задачи.
Этот код демонстрирует, как использовать модуль threading для конкурентной работы двух задач. Основная программа инициирует два потока t1 и t2, где каждый отвечает за выполнение своей определенной задачи. Потоки работают параллельно, и код предоставляет информацию об идентификаторе процесса (process ID) и именах потоков (thread names). Модуль os используется для доступа к process ID, и модуль threading используется для управления потоками и их выполнением.
Итак, мы кратко рассмотрели многопоточность в Python. В следующей статье этой серии рассматривается синхронизация между несколькими потоками [2].
[Python ThreadPool]
Пул потоков (thread pool) это коллекция потоков, которые создаются заранее и могут быть повторно использованы для выполнения нескольких задач. Модуль concurrent.futures в Python предоставляет класс ThreadPoolExecutor, который упрощает создание пула потоков и управление им.
В этом примере мы определили функцию worker, которая запустится в потоке. Мы создаем ThreadPoolExecutor с максимум 2 потоками worker. Затем мы отправляем две задачи в пул с помощью метода submit. Мы используем метод shutdown, чтобы дождаться завершения всех задач до продолжения main thread.
Многопоточность может помочь более эффективные и отзывчивые программы. Однако следует быть осторожным с потоками, чтобы избежать проблем гонки (race conditions) и взаимной блокировки (deadlocks).
Этот код использует пул потоков, созданный с помощью concurrent.futures.ThreadPoolExecutor, чтобы запустить две рабочие задачи (worker tasks) в конкурентном режиме (т. е. параллельно). Основной поток (main thread) ждет завершения потоков worker с помощью использования pool.shutdown(wait=True). Это позволяет достичь эффективной параллельной обработки задач в многопоточном окружении.
import concurrent.futures
defworker():
print("Worker thread работает")
pool = concurrent.futures.ThreadPoolExecutor(max_workers=2)
pool.submit(worker)
pool.submit(worker)
pool.shutdown(wait=True) print("Main thread продолжил работу")
Вывод программы:
Worker thread работает Worker thread работает Main thread продолжил работу
Python хорош для задач ввода/вывода с многопоточностью благодаря технологии Global Interpreter Lock (GIL), которая ограничивает выполнение только одного потока одновременно. Для задач, связанных с CPU, мультипроцессорная обработка часто более эффективна.