В этой статье рассматриваются основы многопоточности на языке программирования Python. Как и многопроцессорная обработка, многопоточность — это способ достижения многозадачности. В многопоточности используется концепция потоков.
Давайте сначала разберемся в концепции потока в компьютерной архитектуре.
В вычислительной технике процесс — это экземпляр выполняемой компьютерной программы. Любой процесс состоит из 3 основных компонентов:
- Исполняемая программа.
- Связанные данные, необходимые программе (переменные, рабочее пространство, буферы и т. д.)
- Контекст выполнения программы (состояние процесса)
Поток — это объект в процессе, который может быть запланирован для выполнения. Кроме того, это наименьшая единица обработки, которая может быть выполнена в ОС (операционной системе).
Проще говоря, поток-это последовательность таких инструкций в программе, которые могут выполняться независимо от другого кода. Для простоты можно предположить, что поток-это просто подмножество процесса!
Поток содержит всю эту информацию в блоке управления потоком (TCB):
- Идентификатор потока: Каждому новому потоку присваивается уникальный идентификатор (TID).
- Указатель стека: Указывает на стек потока в процессе. Стек содержит локальные переменные в области видимости потока.
- Счетчик программ: регистр, в котором хранится адрес инструкции, выполняемой в данный момент потоком.
- Состояние потока: может быть запущено, готово, ждет, запущено или завершено.
- Набор регистров потока: регистры, назначенные потоку для вычислений.
- Указатель родительского процесса: Указатель на блок управления процессом (PCB) процесса, на котором живет поток.
Рассмотрим приведенную ниже диаграмму, чтобы понять взаимосвязь между процессом и его потоком:
Многопоточность
В одном процессе может существовать несколько потоков, в которых:
- Каждый поток содержит свой собственный набор регистров и локальные переменные (хранящиеся в стеке).
- Все потоки процесса совместно используют глобальные переменные (хранящиеся в куче) и программный код.
Рассмотрим приведенную ниже диаграмму, чтобы понять, как в памяти существует несколько потоков:
Многопоточность определяется как способность процессора выполнять несколько потоков одновременно.
В простом одноядерном процессоре это достигается за счет частого переключения между потоками. Это называется переключение контекста. При переключении контекста состояние потока сохраняется, а состояние другого потока загружается всякий раз, когда происходит какое-либо прерывание (из-за ввода-вывода или заданного вручную). Переключение контекста происходит так часто, что все потоки, похоже, работают параллельно (это называется как многозадачность).
Рассмотрим приведенную ниже диаграмму, на которой процесс содержит два активных потока:
Многопоточность в Python
В Python, threading модуль предоставляет очень простой и интуитивно понятный API для создания нескольких потоков в программе.
Рассмотрим простой пример использования модуля threading :
# Python program to illustrate the concept
# of threading
# importing the threading module
import threading
def print_cube(num):
"""
function to print cube of given num
"""
print("Cube: {}".format(num * num * num))
def print_square(num):
"""
function to print square of given num
"""
print("Square: {}".format(num * num))
if __name__ == "__main__":
# creating thread
t1 = threading.Thread(target=print_square, args=(10,))
t2 = threading.Thread(target=print_cube, args=(10,))
# starting thread 1
t1.start()
# starting thread 2
t2.start()
# wait until thread 1 is completely executed
t1.join()
# wait until thread 2 is completely executed
t2.join()
# both threads completely executed
print("Done!")
Square: 100
Cube: 1000
Done!
Давайте попробуем разобраться в приведенном выше коде:
Чтобы импортировать модуль нарезания резьбы, мы делаем:
import threading
Чтобы создать новый поток, мы создаем объект из Класс потоков. Для этого требуются следующие аргументы:
- target: функция, выполняемая потоком
- args: аргументы, которые будут переданы целевой функции
В приведенном выше примере мы создали 2 потока с различными целевыми функциями:
t1 = threading.Thread(target=print_square, args=(10,))
t2 = threading.Thread(target=print_cube, args=(10,))
- Чтобы запустить поток, мы используем метод start класса Thread.
t1.start()
t2.start()
- Как только потоки запускаются, текущая программа (вы можете думать о ней как о главном потоке) также продолжает выполняться. Чтобы остановить выполнение текущей программы до завершения потока, мы используем метод соединения.
t1.join()
t2.join()
В результате текущая программа сначала дождется завершения t1, а затем t2. Как только они будут завершены, будут выполнены остальные инструкции текущей программы.
Рассмотрим приведенную ниже диаграмму, чтобы лучше понять, как работает вышеприведенная программа:
Рассмотрим приведенную ниже программу на python, в которой мы печатаем имя потока и соответствующий процесс для каждой задачи:
# Python program to illustrate the concept
# of threading
import threading
import os
def task1():
print("Task 1 assigned to thread: {}".format(threading.current_thread().name))
print("ID of process running task 1: {}".format(os.getpid()))
def task2():
print("Task 2 assigned to thread: {}".format(threading.current_thread().name))
print("ID of process running task 2: {}".format(os.getpid()))
if __name__ == "__main__":
# print ID of current process
print("ID of process running main program: {}".format(os.getpid()))
# print name of main thread
print("Main thread name: {}".format(threading.current_thread().name))
# creating threads
t1 = threading.Thread(target=task1, name='t1')
t2 = threading.Thread(target=task2, name='t2')
# starting threads
t1.start()
t2.start()
# wait until all threads finish
t1.join()
t2.join()
ID of process running main program: 11758
Main thread name: MainThread
Task 1 assigned to thread: t1
ID of process running task 1: 11758
Task 2 assigned to thread: t2
ID of process running task 2: 11758
Давайте попробуем разобраться в приведенном выше коде:
- Мы используем os.getpid() функция для получения идентификатора текущего процесса.
print("ID of process running main program: {}".format(os.getpid()))
Как видно из выходных данных, идентификатор процесса остается одинаковым для всех потоков
- Мы используем threading.main_thread() функция для получения основного объекта потока. В нормальных условиях основным потоком является поток, из которого был запущен интерпретатор Python. Атрибут name объекта thread используется для получения имени потока.
print("Main thread name: {}".format(threading.main_thread().name))
- Мы используем threading.current_thread() функция для получения текущего объекта потока.
print("Task 1 assigned to thread: {}".format(threading.current_thread().name))
Приведенная ниже диаграмма проясняет вышеприведенную концепцию:
Итак, это было краткое введение в многопоточность в Python. Следующая статья из этой серии посвящена синхронизации между несколькими потоками.