LINUX.ORG.RU

Простой метроном на Python3

 ,


2

3

Доброго времени суток. Пару дней назад начал изучать Python и в качестве первой программы решил реализовать «метроном», который печатает что-нибудь в стандартный вывод раз в какое-то время. Вот скелет моей программы:

#!/usr/bin/python3

import time

while True:
    print('Tick')
    time.sleep(1)
Но даже такая программа ведет себя очень странно: сообщение может задержаться на пару секунд, а потом резко появиться дважды или в течении нескольких тактов ощутимо подтормаживать, а затем внезапно ускориться. Вообщем, работает не стабильно и криво. Может, есть другой, более точный способ реализации цикла с задержкой? Пол дня вчера гуглил на тему «python metronome», везде используют именно time.sleep. Я в растерянности.


Почитай что ли о рилтайме. Тебе никакой линукс без rt-патча «строгий» метроном не даст сделать. Но и «нестрогий» вместо sleep нуждается в select.

Пиши на сях, это проще и надежней.

anonymous ()

Как тут сказали два анонимуса, могут влиять 2 (или 3, кому как больше нравится) фактора. 1) Буферизация вывода, 2) шедулеры ОС с питоновским рантаймом и 3) суть вызывания слипа последовательно с кодом.

Давай выясним, в чем дело. Чуть усовершенствую твой скрипт.

#!/usr/bin/python3

import time

cur_time = lambda: int(round(time.time() * 1000))

while True:
    print('{} tick'.format(cur_time()))
    time.sleep(1)

Ты увидешь что-то вроде. Ты тоже покажи свой вывод

$ ./tick.py 
1501743050511 tick
1501743051512 tick
1501743052513 tick
1501743053514 tick
1501743054515 tick
1501743055516 tick
1501743056517 tick
1501743057518 tick

Числа - время в миллисекундах. Во-первых, обрати внимание на равномерность появления сообщений, как раз то, о чем ты говоришь в своем посте. Они появляются неравномерно, как я понимаю? При этом, числа нам говорят о том, что секунда отсчитывается с точностью до 1-2 миллисекунд.

заменяй строчку c print на

...
print('{} tick'.format(cur_time()), flush=True)
...

Запускай. Сообщения должны теперь появляться более равномерно. Я сейчас не знаю как это автоматически протестировать, поэтому остается полагаться на субьективное восприятие информации с экрана. Полагаю, потребуется специальный стенд для снимков с экрана и анализа равномерности вывода сообщений.

Итак, при flush=True мы уменьшили влияние буфера вывода на равномерность появления сообщений.

Далее, можно заметить, что выводимые значения времени «убегают». Т.е. вывод сообщений получается не через 1.0 секунд, а через 1.001 (или другое значение). Это влияние кумулятивного эффекта от шедулера и того, что слип за слипом вызывается последовательно, перемежаясь с выполенением кода питоновского кода, на которое тоже надо потратить время. На самом деле, влияние шедулера намного меньше, чем может показаться, и дело только в последовательности «слип-код-слип-код».

Усовершенствовал tick2.py

#!/usr/bin/python3

import time
import threading

cur_time = lambda: int(round(time.time() * 1000))

def print_me():
    threading.Timer(1.0, print_me).start()
    print('{} tick'.format(cur_time()), flush=True)

print_me()

Мы не избавились от последовательности «слип-слип-слип» совсем, но исключили «код». И перешли на событийный подход.

$ ./tick2.py 
1501744215613 tick
1501744216613 tick
1501744217614 tick
1501744218614 tick
1501744219614 tick
1501744220614 tick
1501744221614 tick
1501744222615 tick
1501744223615 tick
1501744224615 tick
1501744225615 tick
1501744226615 tick
1501744227615 tick
1501744228616 tick
1501744229616 tick
1501744230616 tick
1501744231616 tick
1501744232616 tick
1501744233617 tick

Намного равномерней!

В итоге, картина мне видится такой: максимальное влияние имеет буферизация вывода (нужно делать Flush=True для избежания). Затем, последовательный вызов кода со слипом; это на гране заметного. А шедулер и рантайм питона имеет минимальное влияние, которое уже не важно для данной задачи.

PS спасибу гуглу и анонимусам за идеи

PtiCa ★★★★★ ()
Ответ на: комментарий от PtiCa

Решение - не писать rt на питоне.

Тут нужна сишка/C++/rust, при том вся работа должна быть в разных потоках, иначе будет накапливаться отставание.

RazrFalcon ★★★★ ()
Последнее исправление: RazrFalcon (всего исправлений: 1)
Ответ на: комментарий от RazrFalcon

Анонимус, упоминавший «реалтайм» и сишечку, занимается преждевременной оптимизацией и забегает не в ту степь, но дает толковый совет про select, конечно. Нужно понимать, как работают библиотеки питона «под капотом» и как работают ОС. А отсутствие flush и последовательный sleep c кодом можно и в Сишечке наблюдать.

А ты даже не потрудился понять, в чем дело, что говорит о том, что твой код на расте будет не эффективней кода ТС на питоне, или вооон того парня на PHP

PtiCa ★★★★★ ()
Ответ на: комментарий от PtiCa

Спасибо за развернутый ответ. Запустил твои примеры:

1)

...
1501746730544 tick
1501746731545 tick
1501746732546 tick
1501746733548 tick
1501746734549 tick
1501746735550 tick
1501746736551 tick
1501746737552 tick
1501746738553 tick
1501746739554 tick
1501746740556 tick
1501746741556 tick
1501746742558 tick
1501746743559 tick
1501746744560 tick
1501746745561 tick
1501746746562 tick
...
2)
...
1501747341541 tick
1501747342541 tick
1501747343542 tick
1501747344542 tick
1501747345543 tick
1501747346543 tick
1501747347544 tick
1501747348544 tick
1501747349545 tick
1501747350545 tick
1501747351546 tick
1501747352546 tick
1501747353547 tick
1501747354547 tick
1501747355548 tick
1501747356548 tick
1501747357549 tick
...
Вывод на экран по-прежнему тормозит но я понял, что это неочевидные тормоза эмулятора терминала. В текстовой консоли без иксов печатает без рывков. Это не критично, т.к. задача метронома издавать звуки, просто неравномерность вывода смутила. Второй пример я вообще не понял (не дошел еще до потоков), так что пока обойдусь слипом. В любом случае, спасибо тебе и анонам.

dots ()
Ответ на: комментарий от RazrFalcon

Решение - не писать rt на питоне.

У тс нет рт.

Тут нужна сишка/C++/rust, при том вся работа должна быть в разных потоках, иначе будет накапливаться отставание.

Мусье не порите чушь. Потоки также будут дрифтить. Единственное решение, учитывать отклонение.

import time


def sleep(until):
    duration = until - time.time()
    if duration > 0:
        time.sleep(duration)


def intervals(period):
    now = time.time()
    while True:
        now += period
        yield now


for until in intervals(1):
    sleep(until)
    now = time.time()
    print('now: {}, diff: {}'.format(now, now - until, flush=True))

ТС, вот «метроном» который не убегает.

anonymous ()
Ответ на: комментарий от RazrFalcon

Без тестов не узнать.

Там инвариант явный. Поэтому формально доказывается что убегания не будет. Более того, этот инвариант позволяет выполнять произвольную работу между sleep, и все равно убегания не будет, если среднее время работы меньше периода.

Но это явно выше твоего уровня. Учитесь, мусье, вам еще много грызть.

anonymous ()

Раз идет речь про обучение, то советую абстрагироваться от задачи метронома и пилить идеально работающий таймер (без привязки к потокам ввода-вывода), который будет выполнять всего 1 задачу - дергать коллбек в нужное время (асинхрнонно, конечно же). Буферизация-шмуферизация - это проблема уже из другой плоскости. Пусть её решает тот, кто отвечает за функцию-коллбек. Не твоя проблема (по крайней мере, пока ты работаешь над таймером). Учись.

anonymous ()
Ответ на: комментарий от anonymous

Типичный sleep в языках программирования гарантирует что поток «проспит» не меньше указанного в аргументе.

Ничего подобного. Поток может быть разбужен любым сигналом.

aureliano15 ()