Когда-то давно мы делали простой таймер с напоминанием на Python. Он работал так:
- Мы спрашивали пользователя, о чём ему напомнить и через сколько минут.
- Программа на это время засыпала и ничего не делала.
- Как только время сна заканчивалось, программа просыпалась и выводила напоминание.
У такой схемы есть минус: мы не можем пользоваться программой и выделенными на неё ресурсами до тех пор, пока она не проснётся. Процессор по кругу гоняет пустые команды и ждёт, когда можно будет продолжить полезную работу. Чтобы процессор и программа могли во время работы таймера делать что-то ещё, используют потоки.
Что такое поток
В упрощённом виде потоки — это параллельно выполняемые задачи. По умолчанию используется один поток — это значит, что программа делает всё по очереди, линейно, без возможности делать несколько дел одновременно.
Но если мы сделаем в программе два потока задач, то они будут работать параллельно и независимо друг от друга. Одному потоку не нужно будет становиться на паузу, когда в другом что-то происходит.
👉 Важно понимать, что поток — это высокоуровневое понятие из области программирования. На уровне вашего «железа» эти потоки всё ещё могут обсчитываться последовательно. Но благодаря тому, что они будут обсчитываться быстро, вам может показаться, что они работают параллельно.
Многопоточность
Представим такую ситуацию:
- У вас на руке смарт-часы, которые собирают данные о вашем пульсе, УФ-излучении и движениях. На смарт-часах работает программа, которая обрабатывает эти данные.
- Программа состоит из четырёх функций. Первая собирает данные с датчиков. Три другие обрабатывают эти данные и делают выводы.
- Пока первая функция не собрала нужные данные, ничего другого не происходит.
- Как только данные введены, запускаются три оставшиеся функции. Они не зависят друг от друга и каждая считает своё.
- Как только все три функции закончат работу, программа выдаёт нужный результат.
А теперь давайте посмотрим, как это выглядит в однопоточной и многопоточной системе. Видно, что если процессор позволяет делать несколько дел одновременно, в многопоточном режиме программа будет работать быстрее:
Многопоточность на Python
За потоки в Python отвечает модуль threading, а сам поток можно создать с помощью класса Thread из этого модуля. Подключается он так:
from threading import Thread
После этого с помощью функции Thread() мы сможем создать столько потоков, сколько нам нужно. Логика работы такая:
- Подключаем нужный модуль и класс Thread.
- Пишем функции, которые нам нужно выполнять в потоках.
- Создаём новую переменную — поток, и передаём в неё название функции и её аргументы. Один поток = одна функция на входе.
- Делаем так столько потоков, сколько требует логика программы.
- Потоки сами следят за тем, закончилась в них функция или нет. Пока работает функция — работает и поток.
- Всё это работает параллельно и (в теории) не мешает друг другу.
Для иллюстрации запустим такой код:
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()
А вот как выглядит результат. Обратите внимание — потоки просыпаются не в той последовательности, как мы их запустили, а в той, в какой их выполнил процессор. Иногда это может помешать работе программы, но про это мы поговорим отдельно в другой статье.
Добавляем потоки в таймер
Первое, что нам нужно сделать, — вынести код таймера-напоминания в отдельную функцию, чтобы создать с ней поток. Для этого используем команду def
:
# Делаем отдельную функцию с напоминанием
def remind():
# Спрашиваем текст напоминания, который нужно потом показать пользователю
print("О чём вам напомнить?")
# Ждём ответа пользователя и результат помещаем в строковую переменную text
text = str(input())
# Спрашиваем про время
print("Через сколько минут?")
# Тут будем хранить время, через которое нужно показать напоминание
local_time = float(input())
# Переводим минуты в секунды
local_time = local_time * 60
# Ждём нужное количество секунд, программа в это время ничего не делает
time.sleep(local_time)
# Показываем текст напоминания
print(text)
Теперь сделаем новый поток, в который отправим выполняться нашу новую функцию. Так как аргументов у нас нет, то и аргументы передавать не будем, а напишем args=().
# Создаём новый поток
th = Thread(target=remind, args=())
# И запускаем его
th.start()
Нам осталось убедиться в том, что пока поток работает, мы можем выполнять в программе что-то ещё и наше засыпание в потоке на это не повлияет. Для этого мы через 20 секунд после запуска выведем сообщение на экран. За 20 секунд пользователь успеет ввести напоминание и время, после чего таймер уйдёт в новый поток и там уснёт на нужное количество минут. Но это не помешает работе основной программы — она тоже будет выполнять свои команды параллельно с потоком.
# Пока работает поток, выведем что-то на экран через 20 секунд после запуска
time.sleep(20)
print("Пока поток работает, мы можем сделать что-нибудь ещё.\n")
Результат:
# Подключаем модуль для работы со временем
import time
# Подключаем потоки
from threading import Thread
# Делаем отдельную функцию с напоминанием
def remind():
# Спрашиваем текст напоминания, который нужно потом показать пользователю
print("О чём вам напомнить?")
# Ждём ответа пользователя и результат помещаем в строковую переменную text
text = str(input())
# Спрашиваем про время
print("Через сколько минут?")
# Тут будем хранить время, через которое нужно показать напоминание
local_time = float(input())
# Переводим минуты в секунды
local_time = local_time * 60
# Ждём нужное количество секунд, программа в это время ничего не делает
time.sleep(local_time)
# Показываем текст напоминания
print(text)
# Создаём новый поток
th = Thread(target=remind, args=())
# И запускаем его
th.start()
# Пока работает поток, выведем что-то на экран через 20 секунд после запуска
time.sleep(20)
print("Пока поток работает, мы можем сделать что-нибудь ещё.\n")
Потоки — это ещё не всё
В Python кроме потоков есть ещё очереди (queue) и управление процессами (multiprocessing). Про них мы поговорим отдельно.