В компьютерных программах многие процессы проходят в несколько потоков или с задействованием разных процессоров. И чтобы реализовать на практике многопоточность, в Пайтоне существует специальный модуль. Он называется threading. С его помощью осуществляется выполнение множества действий, о которых мы сегодня поговорим более подробно. Также мы разберемся, чем многопроцессорность отличается от многопоточности, приведя доступную для понимания метафору.
Понятие потока в информатике
Для начала разберемся в том, что такое вообще поток, независимо от того, какой язык программирования используется. Под этим словом подразумевается простейшая единица работы, которая используется операционной системой для выполнения тех или иных действий.
С помощью потоков реализуется параллельное выполнение некоторых подпрограмм. Впрочем, только на первый взгляд появляется впечатление, что они выполняются параллельно. Конечно, хочется считать, что в приложении работает сразу несколько процессоров, у каждого из которых своя задача в определенный момент. Правда, это немного другое. Даже слово иное – многопроцессорность.
Работа с потоками – это нечто похожее. Тем не менее, здесь приложения работают на одном ядре. Разные действия внутри потоков выполняются лишь на одном процессоре, и для учебных задач его достаточно. А уже операционная система управляет этим процессом.
Как это выразить так, чтобы было более понятно? Представьте, что вы – шахматист-гроссмейстер, который параллельно соревнуется с большим количеством соперников. Он-то на деле и не одномоментно с ними сражается, а просто умело переключается между ними. Тем не менее, шахматист всего один, просто у него быстрый ум.
Так, как выполнение потоков осуществляется на одном процессоре, они прекрасно могут использоваться для убыстрения многих действий. Правда, не каждого. Некоторые из них требуют очень больших расчетов, и в этом случае используется многопроцессорность.
Архитектура вашего приложения бывает такой, которая позволит добиться чистой архитектуры проекта.
Потоки имеют такие свойства:
- Их существование – внутри процесса.
- Один процесс может содержать несколько потоков.
- Потоки, находящиеся в одном процессе, разделяют состояние и память процесса, являющегося родительским по отношению к ним.
Как же доступно изложить смысл модуля threading? Проще всего сделать с применением такого примера кода.
import time from threading import Thread def sleepMe(i): print("Поток %i засыпает на 5 секунд.\n" % i) time.sleep(5) print("Поток %i сейчас проснулся.\n" % i) for i in range(10): th = Thread(target=sleepMe, args=(i, )) th.start()
Если запустить эту последовательность команд, на экране будут отображаться следующие строки.
Поток 0 засыпает на 5 секунд.
Поток 3 засыпает на 5 секунд.
Поток 1 засыпает на 5 секунд.
Поток 4 засыпает на 5 секунд.
Поток 2 засыпает на 5 секунд.
Поток 5 засыпает на 5 секунд.
Поток 6 засыпает на 5 секунд.
Поток 7 засыпает на 5 секунд.
Поток 8 засыпает на 5 секунд.
Поток 9 засыпает на 5 секунд.
Поток 0 сейчас проснулся.
Поток 3 сейчас проснулся.
Поток 1 сейчас проснулся.
Поток 4 сейчас проснулся.
Поток 2 сейчас проснулся.
Поток 5 сейчас проснулся.
Поток 6 сейчас проснулся.
Поток 7 сейчас проснулся.
Поток 8 сейчас проснулся.
Поток 9 сейчас проснулся.
Учтите, что если это приложение запустите вы, то последовательность строк может быть другой. Все потому, что одновременно выполняемые потоки могут не включать определенный процесс.
А теперь давайте приступим к рассмотрению функций, которые использует этот модуль в Python.
Функции threading в Python
Теперь попробуем скорректировать программу, которая есть в примере выше для того, чтобы показать самые разные функции, включенные в threading.
threading.active_count()
Эта функция позволяет разработчику получить количество потоков, которые находятся в момент работы программы на этапе исполнения. Давайте внесем некоторые изменения в программу, чтобы она выглядела следующим образом.
import time import threading from threading import Thread def sleepMe(i): print("Поток %i засыпает на 5 секунд." % i) time.sleep(5) print("Поток %i сейчас проснулся." % i) for i in range(10): th = Thread(target=sleepMe, args=(i, )) th.start() print("Запущено потоков: %i." % threading.active_count())
Теперь вывод будет показывать еще и количество потоков, которые активны на момент исполнения программы.
Поток 0 засыпает на 5 секунд.Запущено потоков: 3.
Запущено потоков: 4.Поток 1 засыпает на 5 секунд.
Запущено потоков: 5.Поток 2 засыпает на 5 секунд.
Поток 3 засыпает на 5 секунд.Запущено потоков: 6.
Запущено потоков: 7.Поток 4 засыпает на 5 секунд.
Поток 5 засыпает на 5 секунд.Запущено потоков: 8.
Поток 6 засыпает на 5 секунд.Запущено потоков: 9.
Запущено потоков: 10.Поток 7 засыпает на 5 секунд.
Поток 8 засыпает на 5 секунд.Запущено потоков: 11.
Поток 9 засыпает на 5 секунд.Запущено потоков: 12.
Поток 0 сейчас проснулся.
Поток 1 сейчас проснулся.
Поток 2 сейчас проснулся.
Поток 3 сейчас проснулся.
Поток 4 сейчас проснулся.
Поток 5 сейчас проснулся.
Поток 6 сейчас проснулся.
Поток 7 сейчас проснулся.
Поток 8 сейчас проснулся.
Поток 9 сейчас проснулся.
Посмотрите теперь на счетчик после запуска всех потоков. Учтите, что он уже показывает число 11. Ничего удивительного в этом нет, поскольку учет основного потока происходит вместе с теми 10, которые исполняются параллельно первому.
threading.current_thread()
С помощью этой функции мы получаем исполняемый поток в данный момент. Она позволяет выполнять определенные действия с инм. Давайте попробуем в наш скрипт внести некоторые изменения.
import time import threading from threading import Thread def sleepMe(i): print("Поток %s засыпает на 5 секунд.\n" % threading.current_thread()) time.sleep(5) print("Поток %s сейчас проснулся." % threading.current_thread()) # Cоздаем только четыре потока for i in range(10): th = Thread(target=sleepMe, args=(i, )) th.start()
После внесения их мы уже получим следующие строки вывода.
Поток <Thread(Thread-4, started 13324)> засыпает на 5 секунд.
Поток <Thread(Thread-1, started 15748)> засыпает на 5 секунд.
Поток <Thread(Thread-5, started 12164)> засыпает на 5 секунд.
Поток <Thread(Thread-2, started 15972)> засыпает на 5 секунд.
Поток <Thread(Thread-6, started 13540)> засыпает на 5 секунд.
Поток <Thread(Thread-3, started 14396)> засыпает на 5 секунд.
Поток <Thread(Thread-7, started 15620)> засыпает на 5 секунд.
Поток <Thread(Thread-8, started 7644)> засыпает на 5 секунд.
Поток <Thread(Thread-9, started 15424)> засыпает на 5 секунд.
Поток <Thread(Thread-10, started 15852)> засыпает на 5 секунд.
Поток <Thread(Thread-4, started 13324)> сейчас проснулся.
Поток <Thread(Thread-1, started 15748)> сейчас проснулся.
Поток <Thread(Thread-5, started 12164)> сейчас проснулся.Поток <Thread(Thread-2, started 15972)> сейчас проснулся.
Поток <Thread(Thread-6, started 13540)> сейчас проснулся.
Поток <Thread(Thread-3, started 14396)> сейчас проснулся.Поток <Thread(Thread-7, started 15620)> сейчас проснулся.
Поток <Thread(Thread-8, started 7644)> сейчас проснулся.Поток <Thread(Thread-9, started 15424)> сейчас проснулся.
Поток <Thread(Thread-10, started 15852)> сейчас проснулся.
threading.main_thread()
С помощью этой функции мы можем получить базовый поток приложения. Именно он учитывается при создании новых потоков. Приведем фрагмент кода, который демонстрирует это.
import threading print(threading.main_thread())
А теперь попробуем запустить эту программу. Увидим такую строку.
<_MainThread(MainThread, started 10476)>
threading.enumerate()
С помощью этой функции мы получаем перечень всех потоков, которые работают в данный момент. В том, чтобы ее использовать, нет абсолютно ничего сложного.
import threading for thread in threading.enumerate(): print("Имя потока %s." % thread.getName())
А теперь попробуем запустить этот скрипт и посмотреть, что получится.
Имя потока MainThread.
Имя потока SockThread.
Видим, что в выводе указывается два потока. В этом нет ничего удивительного или странного, поскольку в тот момент действительно работало только два потока.
threading.Timer()
Эта функция модуля threading используется Python-разработчиками для того, чтобы сгенерировать новый поток и указать период, по прошествии которого должен осуществиться его запуск. После этого потоком запускается определенная функция. Давайте снова приведем пример для большей наглядности.
import threading def delayed(): print("Вывод через 5 секунд!") thread = threading.Timer(5, delayed) thread.start()
А теперь осуществим запуск скрипта.
Вывод через 5 секунд!
Что такое демоны потоков?
Под этим словом подразумевается процесс, который работает в фоне. Python потоки особо значимы для демонов (или как их еще называют, демонических потоков).
Если в приложении запущены потоки, не являющиеся демонами, то сначала программа будет ожидать, пока один поток завершится, чтобы начать следующий. Но потоки, являющиеся демоническими, просто убиваются при завершении работы приложения.
Выводы
Таким образом, мы научились выполнять базовые операции для управления потоками в многопоточной среде. Конечно, эта тема довольно сложная. Мы не рассмотрели много нюансов и непростых моментов. Но этого и не нужно делать. Всегда нужно начинать с простого, и потом уже постепенно усложнять задачу. Только так можно добиться стабильного прогресса в любом навыке, который вы бы хотели освоить. И, конечно, нужно побольше тренироваться. Без этого невозможно добиться даже малейшего прогресса. Поэтому порепетируйте писать код, подобный приведенному выше самостоятельно. Попробуйте изменить приведенные выше программы с учетом ваших имеющихся знаний, проявите креативность. И тогда обязательно все получится.