Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Было бы не плохо этот момент хотя бы немного осветить в этой статье.Да, наверное, вы правы. Какие-то вещи представляются очевидными мне самому, но они не обязаны быть таковыми для всех остальных.
Как мне кажется, за зацикливанием должен следить все таки пользователь библиотеки.В условиях иерархических конечных автоматов и, особенно, наследования, следить может быть слишком сложно. Т.е. вы написали класс агента A с несколькими состояниями, затем ваш коллега отнаследовался от A и ввел еще несколько состояний. Ваш коллега вообще может не знать, что в каком-то своем on_enter/on_exit вы делаете еще одну смену состояния. Так же как и вы не можете знать, что будет происходить в on_enter/on_exit у наследников вашего класса.
А при таких раскладах приходится слать дополнительное, совершенно не нужное, сообщение для смены состояния.А расскажите, пожалуйста, про ваш случай подробнее. Может мы и правда слишком жестко к ограничениям относимся.
Кстати, на счет noexept, so_5::send ведь не noexept?Не noexcept, send может бросать исключения.
А расскажите, пожалуйста, про ваш случай подробнее.
Не noexcept, send может бросать исключения.
Выходит, если нельзя использовать внутри on_enter/on_exit без try/catch (как вы делаете у себя в примере).
Просто логично было бы предположить, что SO будет действовать согласно so_exception_reaction, а не просто вызовет std::terminate.
Сейчас я уже нашел более-менее подходящее решение — вместо перехода в статус st_wait_command вызывать отдельный метод, который решит нужно ли менять состояние или же начать выполнять новую команду. Это работает, но эстетически смущает.
И тут бы идеально подошел time_limit для состояния st_wait_perform.Ну вот тут я не уверен. time_limit хорош для безусловного перехода в другое состояние, когда не приходится при выходе анализировать, что успели сделать, что не успели.
class io_performer : public so_5::agent_t {
struct io_timeout : public so_5::signal_t {};
...
void on_next_operation(mhood_t<start_next_io>) {
// Начинаем отсчет времени для очередной операции.
so_5::send_delayed<io_timeout>(*this, ...);
// Начинаем саму IO-операцию.
perform_io(...);
}
void on_io_result(mhood_t<io_result> cmd) {
... // Должным образом обрабатываем результат.
if(has_more_io_ops())
so_5::send<start_next_io>(*this, ...);
}
void on_timeout(mhood_t<io_timeout>) {
... // Обрабатываем тайм-аут текущей операции.
}
}; Вот в этом случае у вас отложенные сигналы от предыдущих операций будут восприниматься как тайм-ауты для текущей операции. Самый надежный способ избежать этого на данный момент — это включать в отложенное сообщение какой-то ID текущей операции. Например:class io_performer : public so_5::agent_t {
struct io_timeout : public so_5::message_t {
op_id id_;
io_timeout(op_id id) : id_(std::move(id)) {}
};
...
void on_next_operation(mhood_t<start_next_io>) {
// Начинаем отсчет времени для очередной операции.
current_id_ = create_current_op_id();
so_5::send_delayed<io_timeout>(*this, ..., current_op_id_);
// Начинаем саму IO-операцию.
perform_io(...);
}
void on_io_result(mhood_t<io_result> cmd) {
... // Должным образом обрабатываем результат.
current_op_id_ = null_id; // Сбрасываем ID, т.к. текущая операция завершилась.
if(has_more_io_ops())
so_5::send<start_next_io>(*this, ...);
}
void on_timeout(mhood_t<io_timeout> cmd) {
if(current_op_id_ == cmd->id_) {
... // Обрабатываем тайм-аут текущей операции.
}
}
...
op_id current_op_id_;
};Ну или моя задача не особо на них ложится и я пытаюсь притянуть ее за уши.Ну или же ее можно решить на КА, но за счет дополнительных сообщений и, возможно, еще одного состояния.
st_neutral:
..on_enter: отослать себе check_queue
..msg_check_queue -> если очередь пуста, то идем в st_wait_command, если не пуста, то идем в st_wait_perform;
st_wait_command:
..msg_command -> поставить заявку в очередь, перейти в st_wait_perform;
st_wait_perform:
..on_enter: инициировать первую операцию из очереди;
..msg_io_result: обработать результат, перейти в st_neutral;
..msg_command: поставить заявку в очередь;
..time_limit: перейти в st_io_timedout;
st_io_timeout:
..on_enter: обработать тайм-аут для текущей операции, отослать себе msg_check_queue;
..msg_check_queue: делегировать обработку msg_check_queue состоянию st_neutral;
so_5::timer_id_t timer = so_5::send_periodic<msg>(*this,
std::chrono::milliseconds(250), // Задержка.
std::chrono::milliseconds::zero() // Нет повторения, однократная доставка.
);
...
timer.release(); // Отменили таймер.
Но тут вот в чем проблема, если таймер должен сработать в момент T, то на самом деле отложенное сообщение может стать в очередь получателя в диапазоне [T-d1, T+d2], где d1 и d2 — это очень маленькие величины, но, к сожалению не нулевые.so_5::timer_id_t timer = so_5::send_periodic<msg,TYPEID>(*this,
std::chrono::milliseconds(250),
std::chrono::milliseconds::zero(),
myID // <-- здесь передаём свой ID, который потом придёт к нам в обработчике
);
...
Я просто считал, что [T-d1, T+d2] входит в документированную погрешность, которую нужно учитывать. Разве что, думал, что по таймерам есть гарантия «сработает НЕ РАНЬШЕ заданного времени»(т.е. доставка будет только в момент >=T).Это, кстати, хороший момент. Тут зависит от таймерного механизма. Если нет округления времени срабатывания (например, в механизмах timer_heap и timer_list), то таймер срабатывает только в момент >=T. А вот как для timer_wheel, где T должно попадать в «окно»… Наверное, тоже >=T, но навскидку не вспомню. Кроме того, пользователь может и свой таймерный механизм подсунуть, что у него будет — хз…
Значит всё-таки решение задачи ложных или повторных срабатываний таймера остаётся разработчику.Я бы не сказал, что это «ложные и повторные». Это скорее очередная гримаса многопоточности, особенно во времена реальных многоядерных машин. Ведь два потока действительно могут работать параллельно и независимо, от чего взаимные сочетания «кто кого обогнал, кто от кого отстал» могут принимать самые причудливые формы.
SObjectizer: от простого к сложному. Часть III