Время — фундаментальная физическая и философская категория, связанная с последовательными изменениями состояния вселенной.
Хотя категория времени привычна каждому человеку, в программировании, когда сталкиваемся с временем, возникает много сложных вопросов и противоречий, а также проблем, которые долгое время могут быть скрытыми.
Мы измеряем время с помощью чисел. Чтобы такое вообще было возможно, приходится использовать категорию событие. И тогда мы можем задавать следующие вопросы:
Но очень быстро выясняется, что одной категории событие нам маловато. Что было раньше: Полтавская битва или Северная война? Какой промежуток времени прошёл между Смутой и Северной войной? Само событие может длиться во времени. Чтобы ответить на данные вопросы, приходится использовать категорию момента. Момент это тоже событие, но не имеющее длины во времени (момент начала момента совпадает с моментом конца момента).
Событие «Полтавская битва» это часть события «Северная война». Очевидно, вопрос в такой формулировке лишён смысла. Нам придётся ввести моменты: момент начала и момент конца Северной войны, момент начала и момент конца Полтавской битвы. Далее мы решаем, какие именно моменты нам интересны. В одном случае мы получим, что момент начала войны был раньше момента начала битвы, а в другом, что момент конца войны был позже момента конца битвы.
Чтобы ответить на второй вопрос, мы а) поймём, что было раньше (тут, как мы только что выяснили, тоже могут быть проблемы), Смута или Северная война, б) найдём момент окончания первого события (Смуты) и момент начала второго события (Северная война), в) посчитаем разницу.
Довольно сложным является понятие одновременности. Когда мы слышим, что Саша и Таня в субботу одновременно бегали на лыжах, мы понимаем, что в этот день был хотя бы один момент времени, когда и Саша, и Таня бегали на лыжах. Но кто-то может домыслить и представить, что Саша и Таня вместе пришли в лес покататься на лыжах, вместе и ушли. Когда мы слышим, что бегуны Иванов и Петров одновременно пересекли финишную черту на стадионе, мы понимаем, что точности измерения не хватило, чтобы определить победителя.
В первом случае разум человеческий проводит тест на одновременность: берёт произвольный момент на линейке времени и выясняет, что этот момент пересекает два события: «Саша катается на лыжах» и «Таня катается на лыжах». Как ни повышай точность измерения, всё равно будет, что Саша и Таня одновременно бегали на лыжах. Во втором случае это всего лишь артефакт измерения, эта самая одновременность возникает из ничего: вот её не было, но мы понизили точность измерения, и она появилась.
Если мы считаем время в годах, тогда получится, что Анри Пуанкаре умер одновременно с крушением «Титаника», а Наполеон захватил Москву одновременно со своим бегством из России. os:system_time(second) - os:system_time(second) будет почти всегда равно 0, а os:system_time(nanosecond) - os:system_time(nanosecond) — никогда.
Одно дело — точность измерения, другое дело — единицы измерения. В одном месте программы, может быть, мы измеряем в секундах, а в другом месте — в наносекундах. И вот при переходе в другие единицы измерения мы можем получить артефакт одновременности.
Чтобы иметь возможность привязывать к событиям те или иные числа на временной шкале, придётся для начала определить точку отсчёта (нулевое время). Например, это может быть полночь 1 января 1970 года (начало эпохи Unix). Разница, измеренная с помощью некоторого эталона (повторяющегося события), и будет искомым временем нашего события.
В программировании со временем надо обращаться осторожно и аккуратно. Всегда следует опасаться подводных камней, особенно когда программируем Систему, разные ноды которой находятся на разных компьютерах (может быть, даже в разных часовых поясах). На разных компьютерах часы как правило показывают разное время, иногда время корректируется — это может порождать коллизии. Допустим, момент А произошёл раньше момента Б. Есть какая-то вероятность, что во временных метках будет иначе: словно момент Б случился раньше момента А. Поэтому остро встаёт вопрос о синхронизации времени в распределённой системе.
Вполне возможен такой сценарий. Актор на одной ноде даёт задание актору на другой ноде сделать что-то в течение одной секунды, при этом он указывает абсолютное время, до которого надо успеть уложиться (например, 1769217272). Может быть так, что это задание никогда не выполнится, потому что на другой ноде момент 1769217272 уже давно прошёл. Конечно, в данном случае всегда надо указывать относительное время (время таймаута), но проблемы могут быть и иные, более тонкие.
Сложность работы со временем усугубляется ещё тем, что у актора может быть субъективное время (субъективная временная шкала). Допустим, мы создали многопользовательскую игру, в которой игровые сутки проходят за 15 реальных минут. Тут вроде бы понятно: надо просто научиться переводить общесистемное время в игровое. При этом надо учесть, что при перезагрузке игры игровое время должно сохраниться: если игра довольно долго не работала, внутри неё должен сохраниться вечер, если до этого был вечер.
Однако не следует забывать, что любой актор, делающий что-то периодически, обладает этим самым субъективным временем. Допустим, актор должен ежеминутно посылать какие-то важные сигналы другому актору, но если делать это чаще или реже, то возникнет сбой. И вот актор падает в силу каких-то причин, надзиратель его восстанавливает, но состояние таймера пропадает, тогда интервал между сигналами может составить критические 1 или 119 секунд.
Субъективное время акторов следует хорошо шкалировать. Другой пример. Нам по-прежнему требуется посылать какой-то сигнал строго раз в минуту. Для этого мы можем использовать receive ... after 60_000 ... end или таймеры из gen_server или gen_statem. Но будет ли и правда сигнал посылаться ровно раз в минуту? Вовсе нет. Во-первых, какое-то входящее событие может сбить таймер (вплоть до того, что вообще никакой сигнал не будет посылаться). Во-вторых, между запусками таймера, возможно, мы вставим ещё какие-то действия, и эти действия на какое-то время займут актор, и он будет слать сигнал не раз в 60 секунд, а раз в 61 или 65 секунд.
Мы можем решить вторую проблему, если задействуем подходящую функцию из модуля timer. Но опять тогда всплывёт первая проблема, если мы решим сохранять состояние таймера между запусками актора. Ведь состояние таймера из модуля timer мы не можем прочитать. Можем прочитать состояние таймера из модуля erlang, но там нет периодических заданий. Поэтому придётся как-то выкручиваться.
Компьютерные часы, конечно, не идеальны, и чем дальше, тем больше у них разница с объективным временем. Поэтому в большинстве систем часы автоматически корректируются с помощью NTP. Это может привести к другой проблеме: быстрым скачкам времени вперёд или назад. Это может и не иметь никаких негативных последствий, а может быть и сильно наоборот. В качестве прротиводействия этим скачкам в Эрланге используется монотонное время.
Желательно, конечно, чтобы на всех компьютерах Системы работа со временем осуществлялась в одном ключе. В частности, чтобы время RTC было установлено глобальное, а не локальное. Системное время должно синхронизироваться. Текущие настройки можно посмотреть с помощью timedatectl:
Local time: Вт 2024-12-31 19:46:05 +07
Universal time: Вт 2024-12-31 12:46:05 UTC
RTC time: Вт 2024-12-31 12:46:05
Time zone: Asia/Krasnoyarsk (+07, +0700)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Для синхронизации времени следует установить ntp, не забыв включить службу ntpd.service. После этого запускаем timedatectl set-ntp true.
Чтобы узнать, какое сейчас юникс-время (POSIX time), можно выполнить команду date +%s или же функцию os:system_time/0. В первом случае результат будет в секундах, во втором — наносекундах. Впрочем, для этой функции в качества аргумента можно добавить временной юнит second, чтобы получить результат в секундах.
Так мы узнаем системное время. Во всех часовых поясах оно одно и то же. Чтобы убедиться в этом, можно набрать в поисковике “UTC timestamp now” и найти ссылку, где отображается текущее юникс-время. После этого сравнить с тем, что выдаёт date +%s или os:system_time(second).
Стандартным в Эрланге для отображения времения является тип datetime():
-type datetime() :: {date(), time()}.
Каждый из двух типов — date() и time() — в свою очередь тоже являются кортежами:
-type date() :: {year(), month(), day()}. -type time() :: {hour(), minute(), second()}.
Предположим, у нас такая задача: сначала в переменную Now сохранить текущее системное время, потом отобразить это в более наглядном варианте: локальное время и время по Гринвичу…
1> Now = os:system_time(second).
1735661771
2> calendar:system_time_to_local_time(Now, second).
{{2024,12,31},{23,16,11}}
3> calendar:system_time_to_universal_time(Now, second).
{{2024,12,31},{16,16,11}}
Мы получили два кортежа: {{2024,12,31},{23,16,11}} и {{2024,12,31},{16,16,11}}. Первый показывает дату и время в нашем родном часовом поясе, а второй — сколько сейчас в районе Гринвича. Каждый из этих кортежей, как мы видим, тоже состоит из двух кортежей. Один нужен для даты, другой для времени.
Copyright © 2026 Алексей Карманов