Вступление
Задержка кода (также известная как « спящий» ) - это именно то, что подразумевает название, задержка выполнения кода на некоторое время. Наиболее частая потребность в отложении кода - это когда мы ждем завершения какого-либо другого процесса, чтобы мы могли работать с результатом этого процесса. В многопоточных системах потоку может потребоваться дождаться, пока другой поток закончит операцию, чтобы продолжить работу с этим результатом.
Другой пример - снижение нагрузки на сервер, с которым мы работаем.
Например, при парсинге веб-страниц (этически) и соблюдении ToS
рассматриваемого веб-сайта с соблюдением robots.txt
- вы вполне можете
отложить выполнение каждого запроса, чтобы не перегружать ресурсы
сервера.
Многие запросы, отправляемые в быстрой последовательности, могут, в зависимости от рассматриваемого сервера, быстро задействовать все свободные соединения и фактически превратиться в DoS-атаку . Чтобы дать передышку, а также чтобы убедиться, что мы не оказываем негативного воздействия ни на пользователей веб-сайта, ни на сам веб-сайт - мы ограничили количество отправляемых запросов, задерживая каждый из них.
Студент, ожидающий результатов экзамена, может яростно обновлять сайт своей школы в ожидании новостей. В качестве альтернативы они могут написать сценарий, который проверяет, есть ли на веб-сайте что-нибудь новое. В некотором смысле, задержка кода технически может стать планированием кода с допустимым циклом и условием завершения - при условии, что действующий механизм задержки не блокирует .
В этой статье мы рассмотрим, как отложить выполнение кода в Python - также известное как спящий режим . Это можно сделать несколькими способами:
- Код задержки с time.sleep ()
- Код задержки с asyncio.sleep ()
- Код задержки с таймером потоковой передачи
- Код задержки с событием
Код задержки с time.sleep ()
Одним из наиболее распространенных решений проблемы является функция
sleep()
встроенного модуля time
Он принимает количество секунд, в
течение которых процесс должен засыпать - в отличие от многих других
языков, которые основаны на миллисекундах :
import datetime
import time
print(datetime.datetime.now().time())
time.sleep(5)
print(datetime.datetime.now().time())
Это приводит к:
14:33:55.282626
14:34:00.287661
Совершенно ясно, что мы можем видеть задержку в 5 секунд между двумя
print()
с довольно высокой точностью - вплоть до второго десятичного
знака. Если вы хотите спать менее 1 секунды, вы также можете легко
передать нецелые числа:
print(datetime.datetime.now().time())
time.sleep(0.25)
print(datetime.datetime.now().time())
14:46:16.198404
14:46:16.448840
print(datetime.datetime.now().time())
time.sleep(1.28)
print(datetime.datetime.now().time())
14:46:16.448911
14:46:17.730291
Однако имейте в виду, что с двумя десятичными знаками продолжительность
сна может быть не совсем точной , особенно из-за того, что ее трудно
проверить, учитывая тот факт, что print()
требуют некоторого
(переменного) времени для выполнения.
time.sleep()
есть один серьезный недостаток, очень заметный в
многопоточных средах.
time.sleep () блокируется .
Он захватывает поток, в котором находится, и блокирует его на время сна . Это делает его непригодным для более длительного ожидания, так как в течение этого периода он забивает поток процессора. Кроме того, это делает его непригодным для асинхронных и реактивных приложений , которым часто требуются данные и обратная связь в реальном времени.
Еще одно замечание о time.sleep()
- это то, что вы не можете его
остановить. После запуска вы не можете отменить его извне, не завершив
всю программу или sleep()
генерировать исключение, которое
остановило бы его.
Асинхронное и реактивное программирование
Асинхронное программирование вращается вокруг параллельного выполнения, когда задача может выполняться и завершаться независимо от основного потока.
В синхронном программировании - если функция A вызывает функцию B , она останавливает выполнение до тех пор, пока функция B не завершит выполнение, после чего функция A может возобновиться.
В асинхронном программировании - если функция A вызывает функцию B , независимо от ее зависимости результата от функции B , обе могут выполняться одновременно, и, если необходимо, дождаться завершения другой, чтобы использовать результаты друг друга.
Реактивное программирование - это подмножество асинхронного программирования , которое запускает выполнение кода реактивно , когда данные представлены, независимо от того, занята ли функция, которая должна их обрабатывать. Реактивное программирование в значительной степени опирается на архитектуры, управляемые сообщениями (где сообщение обычно является событием или командой ).
И асинхронные, и реактивные приложения сильно страдают от блокировки
кода, поэтому использование чего-то вроде time.sleep()
для них не
подходит. Давайте посмотрим на некоторые варианты задержки
неблокирующего кода.
Код задержки с asyncio.sleep ()
Asyncio - это библиотека Python, предназначенная для написания
параллельного кода и использующая async
/ await
, который может
быть знаком разработчикам, которые использовали его на других языках.
Установим модуль через pip
:
$ pip install asyncio
После установки мы можем import
его в наш скрипт и переписать нашу
функцию:
import asyncio
async def main():
print(datetime.datetime.now().time())
await asyncio.sleep(5)
print(datetime.datetime.now().time())
asyncio.run(main())
При работе с asyncio
мы помечаем функции, которые выполняются
асинхронно, как async
, и await
результатов таких операций, как
asyncio.sleep()
которые будут завершены в какой-то момент в будущем.
Как и в предыдущем примере, это будет напечатано два раза с интервалом в 5 секунд:
17:23:33.708372
17:23:38.716501
Хотя на самом деле это не иллюстрирует преимущества использования
asyncio.sleep()
. Давайте перепишем пример для параллельного
выполнения нескольких задач, где это различие гораздо более ясно:
import asyncio
import datetime
async def intense_task(id):
await asyncio.sleep(5)
print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())
async def main():
await asyncio.gather(
asyncio.create_task(intense_task(1)),
asyncio.create_task(intense_task(2)),
asyncio.create_task(intense_task(3))
)
asyncio.run(main())
Здесь у нас есть async
функция, которая имитирует трудоемкую задачу,
выполнение которой занимает 5 секунд. Затем, используя asyncio
, мы
создаем несколько задач . Однако каждая задача может выполняться
асинхронно, только если мы вызываем их асинхронно. Если бы мы
запускали их последовательно, они также выполнялись бы последовательно.
Для их параллельного вызова мы используем gather()
, которая собирает
задачи и выполняет их:
1 Running some labor-intensive task at 17:35:21.068469
2 Running some labor-intensive task at 17:35:21.068469
3 Running some labor-intensive task at 17:35:21.068469
Все они выполняются одновременно, и время ожидания для трех из них не 15 секунд, а 5.
С другой стороны, если бы мы настроили этот код, чтобы вместо
time.sleep()
import asyncio
import datetime
import time
async def intense_task(id):
time.sleep(5)
print(id, 'Running some labor-intensive task at ', datetime.datetime.now().time())
async def main():
await asyncio.gather(
asyncio.create_task(intense_task(1)),
asyncio.create_task(intense_task(2)),
asyncio.create_task(intense_task(3))
)
asyncio.run(main())
Мы будем ждать 5 секунд между каждым оператором print()
1 Running some labor-intensive task at 17:39:00.766275
2 Running some labor-intensive task at 17:39:05.773471
3 Running some labor-intensive task at 17:39:10.784743
Код задержки с таймером
Класс Timer
- это Thread
, который может запускать и выполнять
операции только по прошествии определенного периода времени. Это именно
то, что мы ищем, хотя использование Thread
s для задержки кода
является излишним, если вы еще не работаете с многопоточной системой.
Класс Timer
должен быть start()
, и его можно остановить с помощью
cancel()
. Его конструктор принимает целое число, обозначающее
количество секунд ожидания перед выполнением второго параметра -
функции.
Создадим функцию и выполним ее через Timer
:
from threading import Timer
import datetime
def f():
print("Code to be executed after a delay at:", datetime.datetime.now().time())
print("Code to be executed immediately at:", datetime.datetime.now().time())
timer = Timer(3, f)
timer.start()
Это приводит к:
Code to be executed immediately at: 19:47:20.032525
Code to be executed after a delay at: 19:47:23.036206
Метод cancel()
очень удобен, если у нас работает несколько функций, и
мы хотели бы отменить выполнение функции на основе результатов другой
или другого условия.
Напишем функцию f()
, которая вызывает как f2()
и f3()
. f2()
вызывается как есть - и возвращает случайное целое число от 1 до 10,
имитируя время, необходимое для запуска этой функции.
f3()
вызывается через Timer
и если результат f2()
больше 5
,
f3()
отменяется, тогда как если f2()
выполняется в «ожидаемое» время
меньше 5
- f3()
запускается после таймер заканчивается:
from threading import Timer
import datetime
import random
def f():
print("Executing f1 at", datetime.datetime.now().time())
result = f2()
timer = Timer(5, f3)
timer.start()
if(result > 5):
print("Cancelling f3 since f2 resulted in", result)
timer.cancel()
def f2():
print("Executing f2 at", datetime.datetime.now().time())
return random.randint(1, 10)
def f3():
print("Executing f3 at", datetime.datetime.now().time())
f()
Выполнение этого кода несколько раз будет выглядеть примерно так:
Executing f1 at 20:29:10.709578
Executing f2 at 20:29:10.709578
Cancelling f3 since f2 resulted in 9
Executing f1 at 20:29:14.178362
Executing f2 at 20:29:14.178362
Executing f3 at 20:29:19.182505
Код задержки с событием
Класс Event
можно использовать для генерации событий. Одно событие
может быть "прослушано" несколькими потоками. Функция Event.wait()
блокирует поток, в котором она находится, за исключением
Event.isSet()
. После того, как вы set()
событие, все ожидающие
потоки пробуждаются, и Event.wait()
становится неблокирующим .
Это можно использовать для синхронизации потоков - все они накапливаются
и wait()
пока не будет установлено определенное событие, после чего
они могут определять свой поток.
Давайте создадим waiter
и запустим его несколько раз в разных потоках.
Каждый официант начинает работать в определенное время и каждую секунду
проверяет, работают ли они еще час, прямо перед тем, как принять заказ,
на выполнение которого уходит секунда. Они будут работать, пока не будет
назначено Событие, а точнее, их рабочее время не истечет.
У каждого официанта будет свой собственный поток, в то время как управление находится в основном потоке и звонит, когда каждый может позвонить домой. Поскольку сегодня они чувствуют себя очень щедрыми, они сократят рабочее время и отпустят официантов домой через 4 секунды работы:
import threading
import time
import datetime
def waiter(event, id):
print(id, "Waiter started working at", datetime.datetime.now().time())
event_flag = end_of_work.wait(1)
while not end_of_work.isSet():
print(id, "Waiter is taking order at", datetime.datetime.now().time())
event.wait(1)
if event_flag:
print(id, "Waiter is going home at", datetime.datetime.now().time())
end_of_work = threading.Event()
for id in range(1, 3):
thread = threading.Thread(target=waiter, args=(end_of_work, id))
thread.start()
end_of_work.wait(4)
end_of_work.set()
print("Some time passes, management was nice and cut the working hours short. It is now", datetime.datetime.now().time())
Выполнение этого кода приводит к:
1 Waiter started working at 23:20:34.294844
2 Waiter started working at 23:20:34.295844
1 Waiter is taking order at 23:20:35.307072
2 Waiter is taking order at 23:20:35.307072
1 Waiter is taking order at 23:20:36.320314
2 Waiter is taking order at 23:20:36.320314
1 Waiter is taking order at 23:20:37.327528
2 Waiter is taking order at 23:20:37.327528
Some time passes, management was nice and cut the working hours short. It is now 23:20:38.310763
end_of_work
здесь использовалось для синхронизации двух потоков и
управления, когда они работают, а когда нет, задерживая выполнение кода
на установленное время между проверками.
Заключение
В этом руководстве мы рассмотрели несколько способов задержки выполнения кода в Python, каждый из которых применим к разному контексту и требованиям.
time.sleep()
очень полезен для большинства приложений, хотя он не
совсем оптимален для длительного времени ожидания, обычно не
используется для простого планирования и блокирует.
Используя asyncio
, у нас есть асинхронная версия time.sleep()
которую мы можем await
.
Класс Timer
задерживает выполнение кода и при необходимости может быть
отменен.
Класс Event
генерирует события, которые несколько потоков могут
прослушивать и соответственно реагировать, задерживая выполнение кода до
тех пор, пока не будет установлено определенное событие.