Мне нужно выполнить функцию один раз в определенное время дня, как в этом псевдокоде:
while(true)
{
now = Now();
if (now == 16:00 or now == 22:00)
{
call_function();
}
}
Я использую chrono
Timepoints довольно часто, но всегда для относительных арифметических сравнений, например:
if (now >= prev_ts + std::chrono::minutes(30))
{
do_something();
prev_ts = now;
и это не сработает, потому что первый вызов произойдет немедленно, а мне нужно, чтобы это произошло в следующие 16:00.
Каков самый чистый способ добиться этого с помощью C++23?
Что меня смущает, так это то, что меня не волнуют даты и не волнуют смещения. Мне нужно что-то, что является просто объектом времени, а не датой и временем.
@TangentiallyPerpendicle это не отдельный процесс. Данные (из функции) должны быть доступны остальной части приложения.
Если есть какая-то причина, по которой запланированное задание не работает для вас, вам необходимо объяснить это в своем вопросе. Тем не менее, круглосуточно работающий процесс, проверяющий время каждые несколько микросекунд, — это очень плохая практика. Он тратит время процессора и заряд батареи (если это актуально) и может даже не работать, если ОС приостанавливает процесс в периоды простоя.
Самый простой и понятный вариант — использовать std::time
с std::localtime_r из <ctime>
. Это позволит разделить время на часы, минуты и секунды намного проще, чем что-либо в библиотеке <chrono>
. Просто периодически проверяйте время и спите.
Есть ли причина, по которой это помечено как c++23?
@catnip, чтобы показать ответы на C++23, приветствуются, потому что после этого хрон стал намного лучше (c++20?).
Начиная с C++11 (я думаю), вы можете рассмотреть возможность использования std::sleep_for()
или std::sleep_until()
(при запуске определите, как далеко в будущем вы хотите выполнить требуемое действие, и спите столько же). Загвоздка в том, что они переводят текущий поток в спящий режим — если вашей программе нужно делать другие вещи во время ожидания, вам нужно будет явно запланировать эти «другие дела» или выполнить их в другом потоке. Возможно, было бы (ммм) проще запланировать задачу cron/scheduler в вашей системе, чтобы сигнализировать вашей программе, когда это необходимо (и написать код для ответа на этот сигнал).
Вы ищете выполнение по местному времени или по всемирному координированному времени? Это имеет значение, поскольку местное время не гарантируется таким же, как UTC.
@Tangentially Perpendular - есть причины, по которым вы не можете использовать планировщик ОС. Например, у меня есть код в DLL, загружаемый другим приложением, который необходимо выполнять каждую пятницу в 16:00.
Вы можете рассчитать следующее наступление целевого времени и использовать std::this_thread::sleep_until
, чтобы дождаться достижения этого времени.
Рассчитайте следующее появление целевого времени (например, 16:00 и 22:00), затем используйте std::this_thread::sleep_until
, чтобы дождаться следующего целевого времени, и, наконец, вызовите функцию в указанное время.
#include <iostream>
#include <chrono>
#include <thread>
void call_function() {
std::cout << "Function called at " << std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) << std::endl;
}
std::chrono::system_clock::time_point get_next_target_time(int hour, int minute) {
using namespace std::chrono;
auto now = system_clock::now();
auto now_time_t = system_clock::to_time_t(now);
auto now_tm = *std::localtime(&now_time_t);
now_tm.tm_hour = hour;
now_tm.tm_min = minute;
now_tm.tm_sec = 0;
auto target_time = system_clock::from_time_t(std::mktime(&now_tm));
if (target_time <= now) {
now_tm.tm_mday += 1; // Schedule for the next day
target_time = system_clock::from_time_t(std::mktime(&now_tm));
}
return target_time;
}
int main() {
while (true) {
auto next_16 = get_next_target_time(16, 0); // Next 16:00
auto next_22 = get_next_target_time(22, 0); // Next 22:00
auto next_time = std::min(next_16, next_22); // Get the nearest time
std::this_thread::sleep_until(next_time); // Wait until the next target time
call_function();
}
return 0;
}
Это вопрос, на который можно ответить несколькими способами. Какой путь правильный, зависит от потребностей приложения.
В зависимости от ответов на эти вопросы ответ на вопрос ФП варьируется от очень простого до относительно сложного. Независимо от того, насколько это сложно, <chrono>
в C++23 справится с этим.
В своих ответах ниже я использую директивы function-local using, чтобы сократить многословие, которое может быть вызвано повторением std::chrono::
во многих местах. Сохраняя директивы using в области функций, я избегаю загрязнения глобального пространства имен. Однако, если это не ваш стиль, не стесняйтесь добавлять std::chrono::
непосредственно к тем символам, которые в противном случае этого требуют.
Я начну с самого простого ответа и буду развивать его, увеличивая сложность, чтобы справиться с более сложными случаями.
#include <chrono>
#include <iostream>
#include <thread>
// UTC time
template <class F>
void
execute_at_time_of_day(std::chrono::system_clock::duration tod, F f)
{
using namespace std::chrono;
auto now = system_clock::now();
auto time_to_execute = floor<days>(now) + tod;
if (time_to_execute < now)
time_to_execute += days{1};
while (true)
{
std::this_thread::sleep_until(time_to_execute);
f();
time_to_execute += days{1};
}
}
int
main()
{
using namespace std::chrono;
std::thread{[]()
{
execute_at_time_of_day(16h, []() {std::cout << "hi 1\n";});}
}.detach();
std::thread{[t]()
{
execute_at_time_of_day(22h, []() {std::cout << "hi 2\n";});}
}.detach();
while (true) {}
}
Примечания:
Я упростил main
, предположив, что приложение работает вечно. Поэтому нет необходимости включать способ сообщить потокам, что приложению пора завершить работу. В реальном приложении можно было бы включить способ main
сообщить потокам, что пора заканчивать, а затем присоединиться к потокам до завершения.
Я также выделил отдельный поток для каждой пары время суток/функция, чтобы было проще иметь некоторое количество пар, кроме двух.
Время суток (UTC) определяется длительностью хроно, которая может быть грубой, как часы, или точной, как system_clock::duration
, что на практике не менее микросекунд. Например, если бы я хотел изменить 16:00 на 16:05:34.500, то в main
я бы изменил 16h
на 16h + 5min + 34s + 500ms
.
В этом простейшем случае execute_at_time_of_day
сначала получает системное время, сохраняя его в переменной now
. Это время указывается как Unix Time (время с 1970-01-01 00:00:00 UTC, исключая дополнительные секунды).
Выражение floor<days>(now)
усекает текущее время до точности дней (округляя вниз). Фактически это дата UTC, которая также может служить отсчетом времени до полуночи, с которой начинается эта дата UTC.
К дате UTC добавляется переданное время суток. Это желаемое время для следующего выполнения функции f
. Если это время уже в прошлом, добавляется 1 день.
В цикле мы спим до желаемого времени для выполнения функции. Затем выполняем функцию. Затем мы вычисляем время следующего выполнения функции, добавляя 1 день.
Если в качестве времени суток указано местное время устройства, execute_at_time_of_day
становится немного сложнее.
// Local time / Stationary device
template <class F>
void
execute_at_time_of_day(std::chrono::system_clock::duration local_tod, F f)
{
using namespace std::chrono;
auto zone = current_zone();
auto now_utc = system_clock::now();
auto time_to_execute = zone->to_local(now_utc);
time_to_execute = floor<days>(time_to_execute) + local_tod;
if (zone->to_sys(time_to_execute) < now_utc)
time_to_execute += days{1};
while (true)
{
std::this_thread::sleep_until(zone->to_sys(time_to_execute));
f();
time_to_execute += days{1};
}
}
Примечания:
Выражение current_zone()
возвращает time_zone const*
, который относится к текущему часовому поясу устройства. Это можно использовать для перевода между UTC и местным временем. Результат сохраняется в переменной zone
.
Если вместо текущего местного часового пояса устройства вам нужен конкретный часовой пояс IANA, вы можете изменить эту строку:
auto zone = current_zone();
чтобы (например):
auto zone = locate_zone("Europe/London");
В этой версии execute_at_time_of_day
time_to_execute
вычисляется по местному времени, а не по UTC. Это делается путем перевода текущего времени на местное время с помощью zone
. Затем (как и раньше) execute_at_time_of_day
устанавливается с точностью до дней и к нему добавляется желаемое время суток. А если это время уже в прошлом, добавляются сутки.
Проверка «в прошлом» выполняется в формате UTC, а не по местному времени, поскольку время UTC, как известно, является монотонной шкалой, в отличие от местного времени, которое может скакать из-за корректировок перехода на летнее время (или любых других корректировок, инициированных политиками).
В цикле time_to_execute
переводится в формат UTC для оператора sleep_until
. sleep_until
не сможет скомпилироваться, если указано местное время. Это снова мотивировано немонотонностью местного времени.
После вызова функции время следующего выполнения вычисляется по местному времени, а не по UTC. Это сохраняет местное время суток даже за пределами летнего времени. Т.е. следующий вызов функции обычно происходит через 24 часа, но может быть и через 23 или 25 часов, в зависимости от правил перехода на летнее время. Если бы это вычисление было выполнено в формате UTC, следующий вызов функции всегда будет ровно через 24 часа.
Возможно (но маловероятно), что указанное местное время имеет неуникальное сопоставление с UTC. Это может произойти, когда (например) корректировка смещения UTC из-за перехода на летнее время пропускает местное время или создает два из них. Если это произойдет, будет выброшено исключение. Если исключение нежелательно, есть способы сделать это. Но я оставлю это для другого вопроса.
Если это устройство иногда может быть перенесено в другой часовой пояс, и если функция может выполняться в неподходящее время, когда это происходит, измените эту строку:
std::this_thread::sleep_until(zone->to_sys(time_to_execute));
к:
std::this_thread::sleep_until(current_zone()->to_sys(time_to_execute));
Т.е. повторно запрашивайте местный часовой пояс на каждой итерации цикла.
Если известно, что устройство меняет часовые пояса, и нужно, чтобы устройство успевало успевать за изменением часовых поясов и выполнялось в нужное время даже сразу после такого изменения, то все значительно усложняется.
В приведенном ниже решении поток ничего не делает, кроме проверки того, изменился ли местный часовой пояс. И когда оно меняется, переменная глобальной области, содержащая time_zone const*
, сообщает, какой локальный часовой пояс между потоками, а mutex
и condition_variable
глобальной области синхронизируют эту связь потокобезопасным способом:
// Local time / For mobile device
auto zone = std::chrono::current_zone();
std::mutex zone_mutex;
std::condition_variable zone_cv;
void
check_for_zone_change()
{
using namespace std::chrono;
while (true)
{
auto new_zone = current_zone();
if (zone != new_zone)
{
std::lock_guard lock(zone_mutex);
zone = new_zone;
zone_cv.notify_all();
}
// Adjust this to however responsive you want to be
// to changes in time zone
std::this_thread::sleep_for(1min);
}
};
Все, что делает эта функция, — это время от времени просыпается и смотрит, изменилось ли значение возврата от current_zone()
. Если да, то он изменяет глобальную переменную zone
и уведомляет другие потоки с помощью zone_cv
condition_variable
. У меня проверка раз в минуту. Вы можете установить частоту опроса на любую, подходящую для вашего приложения.
execute_at_time_of_day
теперь изменится на:
template <class F>
void
execute_at_time_of_day(std::chrono::system_clock::duration local_tod, F f)
{
using namespace std::chrono;
std::unique_lock lock{zone_mutex};
auto now_utc = system_clock::now();
auto time_to_execute = zone->to_local(now_utc);
time_to_execute = floor<days>(time_to_execute) + local_tod;
if (zone->to_sys(time_to_execute) < now_utc)
time_to_execute += days{1};
while (true)
{
if (zone_cv.wait_until(lock,
zone->to_sys(time_to_execute)) == std::cv_status::timeout ||
zone->to_sys(time_to_execute) < system_clock::now())
{
lock.unlock();
f();
time_to_execute += days{1};
lock.lock();
}
}
}
Примечания:
Эта функция должна быть zone_mutex
заблокирована каждый раз, когда она читает (использует) глобальную zone
, то есть все время, кроме времени, когда она находится в режиме ожидания или выполнения f
.
Инициализация time_to_execute
работает как и раньше. Но метод сна изменился. Во время сна мы хотим иметь возможность проснуться по двум причинам:
Для этого мы «спим» на condition_variable
с wait_until
. Если время ожидания истекло, значит прошли сутки, в противном случае нас уведомили о смене часового пояса. Кроме того, природа условных переменных такова, что потоки могут неожиданно пробуждаться. Таким образом, код должен быть устойчив к возможности того, что он просыпается, а ни дня не прошло, ни часовой пояс не изменился.
Если часовой пояс изменился, желаемое местное время выполнения может все еще быть в будущем или в прошлом. Только если zone_cv
сообщает об истечении времени ожидания или если желаемое время выполнения уже в прошлом, мы хотим выполнить функцию и вычислить новое время выполнения. В противном случае желаемое время выполнения находится в будущем, и у нас либо было ложное пробуждение, либо изменение часового пояса, из-за которого желаемое время выполнения было перенесено в будущее, поэтому мы ничего не делаем, кроме как снова заснуть.
В этом решении main
необходимо изменить, чтобы запустить поток check_for_zone_change
:
std::thread{check_for_zone_change}.detach();
Наконец, если приложение должно работать в течение нескольких месяцев и во время работы обновляться до последней базы данных часовых поясов IANA, вам не повезло в ОС Windows и Apple. Но в Linux это можно сделать с помощью gcc:
См. документацию для:
const std::chrono::tzdb& reload_tzdb();
std::string remote_version();
Вызов reload_tzdb()
обновит вашу базу данных часовых поясов, если она доступна. Последующие вызовы current_zone()
и locate_zone()
будут ссылаться на эту новую базу данных. Прежде чем вызвать reload_tzdb()
, вы можете сравнить remote_version()
с get_tzdb().version
, чтобы узнать, изменит ли повторная загрузка базы данных номер версии базы данных.
Можно создать еще одну ветку для опроса remote_version()
и звонка reload_tzdb()
при обнаружении изменения версии.
Забудьте все это. Напишите код, который вам нужно выполнить один раз, и ваша операционная система запустит вашу программу в запланированное время с помощью
cron
(Linux), планировщика задач (Windows) или любого другого механизма, который использует ваша система.