Запустить асинхронно функцию
Всех приветствую. Ради идеи хочу написать какой-нибудь консольный секундомер. Есть
import os
import asyncio
import time
import logging
logging.basicConfig(filename = "mylog.log", level=logging.DEBUG)
class Timer():
def __init__(self) -> None:
self.minute = 0
self.seconds = 0
self.deciseconds = 0
def add_deciseconds(self):
self.deciseconds += 1
if self.deciseconds >= 10:
self.seconds += 1
self.deciseconds = 0
if self.seconds >= 60:
self.seconds = 0
self.minute += 1
def get_time(self):
return (self.minute, self.seconds, self.deciseconds)
async def tick(my_timer: Timer):
my_timer.add_deciseconds()
os.system("cls")
m , s, ss = my_timer.get_time()
print(f"{m} : {s} : {ss}")
async def main(tim):
await tick(tim)
if __name__ == '__main__':
my_timer = Timer()
while True:
start = time.time()
logging.debug(start)
asyncio.run(main(my_timer))
time.sleep(1)
end = time.time()
logging.debug(end)
logging.debug(end - start)
logging.debug('------------------')
Логи и асинхрон - это уже попытки решить проблему.
Проблема: time.sleep(1) ждёт больше 1 секунды. Ради проверки включал это приложение и через пару секунд включал секундомер на телефоне. Где-то ко второй минуте секундмер с телефона догонял.
Не особо знаю за асинхронное программирование, но я подумал, что код с выводом в терминал времени и повышения на единицу заставляет программу ещё ждать, поэтому попытылся сделать функцию асинхронной. Вдохновлялся шаблоном из документации
Логи при этом выглядят примерно так:
DEBUG:root:1699353460.6091332
DEBUG:asyncio:Using proactor: IocpProactor
DEBUG:root:1699353461.6302617
DEBUG:root:1.0211284160614014 # Разница между старт и стоп больше 1 секунды
DEBUG:root:------------------
DEBUG:root:1699353461.6313007
DEBUG:asyncio:Using proactor: IocpProactor
DEBUG:root:1699353462.666385
DEBUG:root:1.0350842475891113 # Разница между старт и стоп больше 1 секунды
DEBUG:root:------------------
Я почитал разные статьи, в том числе вопросы других людей с SO (Один из примеров). В них говорится о том, что погрешность - это допустимо, но там говорится о погрешности ~0.00001 и меньше.
Я попробовал запустить без всяких таймеров цикл:
while True:
start = time.time()
logging.debug(start)
time.sleep(1)
end = time.time()
logging.debug(end)
logging.debug(end - start)
logging.debug('------------------')
И в этом случае цифры в логе уже были более точными
DEBUG:root:1699355102.4560356
DEBUG:root:1699355103.4560683
DEBUG:root:1.000032663345337
DEBUG:root:------------------
DEBUG:root:1699355103.457032
DEBUG:root:1699355104.4571276
DEBUG:root:1.0000956058502197
DEBUG:root:------------------
Без логирования таймер всё равно чуть медленнее идёт.
Не могу понять, что конкретно тормозит мою программу?
Ответы (2 шт):
Вашу программу тормозит вызов os.system("cls") в первом случае и logging.debug во втором.
time.sleep() добавляет задержку в вашем коде, но сам ваш код выполняется не нулевое время, из-за этого фактическое время выполнения одной итерации будет больше, чем указано в параметре sleep, и таймер постепенно будет отставать от реального времени.
Даже простой замер времени sleep
import time
t = time.time()
time.sleep(1.0)
dt = time.time() - t
print(dt)
покажет фактическую задержку больше 1 секунды (у меня показало 1.000610113143921). При большем количестве кода между замерами (и при наличии более медленного кода, например операций ввода-вывода, в том числе cls) - дополнительная задержка будет больше, и в цикле отставание будет постепенно увеличиваться.
Плюс операционная система не гарантирует абсолютную точность задержки, какой-то процесс может занять ядро надолго, возвращение из "сна" может произойти не в запланированное время.
Если предположить, что фоновых тяжелых процессов нет, можно пересчитывать задержку с учетом фактического времени выполнения итерации. Ну и выводить фактическое прошедшее время (по данным таймера компьютера), а не вручную посчитанное. Прототип таймера с пересчетом задержки, на каждую секунду должно выполняться 10 итераций:
import time
target_dt = 0.1 # Целевой промежуток времени между итерациями (десять итераций в секунду)
dt = target_dt # Целевое значение задержки между итерациями берем как начальное
t = time.time()
prev_time = t
while True:
print(f"{prev_time - t:.3f}, {dt}")
time.sleep(dt)
# time.sleep(0.01) # Даже если специально добавить дополнительную задержку, dt пересчитается так, чтобы ее учитывать
current_time = time.time()
# Фактический промежуток времени между итерациями
dt_fact = current_time - prev_time
# Пересчитываем задержку с поправкой на разницу между фактической задержкой и требуемой
dt = target_dt - (dt_fact - dt)
prev_time = current_time
Вывод:
0.000, 0.1
0.100, 0.09979171752929689
0.200, 0.09970550537109377
0.300, 0.099810266494751
0.400, 0.09973764419555667
0.500, 0.0997503757476807
0.600, 0.09978647232055668
0.700, 0.09971432685852055
0.800, 0.09980025291442876
0.900, 0.09976506233215338
1.000, 0.09972820281982428
1.100, 0.09979052543640143
1.200, 0.09976487159729011
1.300, 0.09973921775817879
1.400, 0.09974193572998055
1.500, 0.0997568130493165
1.600, 0.09974713325500498
1.700, 0.09976415634155283
1.800, 0.09980382919311534
1.900, 0.09972858428955089
2.000, 0.09971747398376477
2.100, 0.09978790283203137
2.200, 0.09974532127380384
2.300, 0.09975781440734877
2.400, 0.09979414939880385
2.500, 0.09974107742309585
2.600, 0.09973020553588882
2.700, 0.09979276657104508
2.800, 0.09974160194396989
2.900, 0.09979963302612321
3.000, 0.09978470802307146
3.100, 0.09977574348449725
3.200, 0.09972982406616229
3.300, 0.0997399330139162
3.400, 0.09973549842834492
3.500, 0.09977755546569844
3.600, 0.09981722831726095
3.700, 0.09977011680603048
3.800, 0.09973802566528342
3.900, 0.09972071647644065
4.000, 0.09967408180236839
...
Видно, что промежуток с точностью около +-0.001 между итерациям сохраняется.
Тестировалось на Linux, на Windows результат может быть другим (вроде бы там нельзя сделать задержку с точностью лучше 0.01 секунды, из-за этого точность таймера может быть хуже).
Помещать таймер в async функцию я не вижу особого смысла. Если в этом же эвент лупе будут выполняться другие функции, это будет добавлять дополнительную случайную погрешность. Если нужно - запускайте его в отдельном потоке.