Upd. Расширенная версия этого текста выложена в виде статьи на Хабре.
Дублирование кода не есть хорошо. Поэтому, когда в коде стали часто повторяться очень похожие фрагменты вида:
class dispatcher_t { ... void work_started() { std::lock_guard< activity_traits::lock_t > lock{ m_stats_lock }; m_is_in_working = true; m_work_started_at = so_5::stats::clock_type_t::now(); m_work_activity.m_count += 1; } void work_finished() { std::lock_guard< activity_traits::lock_t > lock{ m_stats_lock }; m_is_in_working = false; so_5::stats::details::update_stats_from_current_time( m_work_activity, m_work_started_at ); } so_5::stats::activity_stats_t take_work_stats() { so_5::stats::activity_stats_t result; bool is_in_working{ false }; so_5::stats::clock_type_t::time_point work_started_at; { std::lock_guard< activity_traits::lock_t > lock{ m_stats_lock }; result = m_work_activity; if( true == (is_in_working = m_is_in_working) ) work_started_at = m_work_started_at; } if( is_in_working ) so_5::stats::details::update_stats_from_current_time( result, work_started_at ); return result; } }; |
То захотелось вынести все это дело в отдельный вспомогательный класс. С очень простой реализацией:
class stats_collector_t { public : void start() { /* как в показанном выше work_started */ } void stop() { /* как в показанном выше work_finished */ } so_5::stats::activity_stats_t take_stats() { /* как в показанном выше take_work_stats */ } private : activity_traits::lock_t m_lock; bool m_is_in_working{ false }; so_5::stats::clock_type_t::time_point m_work_started_at; so_5::stats::activity_stats_t m_work_activity{}; }; |
Все вроде бы хорошо. Но обнаружилась первая засада: в ряде случаев у stats_collector_t не должно было быть собственного m_lock-а. Например, в ряде диспетчеров создается один объект m_lock, который должен использоваться при работе с двумя разными экземплярами stats_collector_t. Т.е. в каких-то местах stats_collector_t должен иметь собственный m_lock, в других местах должен уметь использовать чужой lock.
Ну не проблема. Преобразуем stats_collector_t в шаблон, параметр которого и будет говорить, используется ли внутренний или внешний lock-объект:
template< LOCK_HOLDER > class stats_collector_t { public : // Тут нам нужен уже конструктор, который будет передавать // какие-то значения в конструктор LOCK_HOLDER-а. // Что это будут за значения и сколько их будет знает только // LOCK_HOLDER, но не знает stats_collector_t. template< typename... ARGS > stats_collector_t( ARGS && ...args ) : m_lock_holder{ std::forward<ARGS>(args)... } {} void start() { std::lock_guard< LOCK_HOLDER > lock{ m_lock_holder }; ... /* остальные действия как показано выше */ } void stop() { std::lock_guard< LOCK_HOLDER > lock{ m_lock_holder }; ... /* остальные действия как показано выше */ } so_5::stats::activity_stats_t take_stats() {...} private : LOCK_HOLDER m_lock_holder; bool m_is_in_working{ false }; so_5::stats::clock_type_t::time_point m_work_started_at; so_5::stats::activity_stats_t m_work_activity{}; }; |
Где в качестве LOCK_HOLDER-ов должны были испльзоваться вот такие простенькие классы:
class internal_lock { activity_traits::lock_t m_lock; public : internal_lock() {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } }; class external_lock { activity_traits::lock_t & m_lock; public : external_lock( activity_traits::lock_t & lock ) : m_lock( lock ) {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } }; |
Ну и инициализироваться stats_collector_t стал тем или иным способом:
class one_dispatcher_t { ... private : // Для случая, когда должен использоваться внешний lock-объект. activity_traits::lock_t m_common_lock; stats_collector_t< external_lock > m_work_stats{ m_common_lock }; stats_collector_t< external_lock > m_wait_stats{ m_common_lock }; ... }; class another_dispatcher_t { ... private : // Для случая, когда должен использоваться внутренний lock-объект. stats_collector_t< internal_lock > m_work_stats{}; stats_collector_t< internal_lock > m_wait_stats{}; ... }; |
Правда, здесь так же обнаружилась засада. Оказалось, что тип внешнего lock-объекта не всегда будет activity_traits::lock_t. Иногда нужно использовать другой тип lock-объекта, который, тем не менее, пригоден для работы с std::lock_guard.
Поэтому вспомогательный класс external_lock так же стал шаблоном:
template< typename LOCK = activity_traits::lock_t > class external_lock { LOCK & m_lock; public : external_lock( LOCK & lock ) : m_lock( lock ) {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } }; |
В результате чего использование stats_collector_t стало выглядеть вот так:
class one_dispatcher_t { ... private : // Для случая, когда должен использоваться внешний lock-объект. activity_traits::lock_t m_common_lock; stats_collector_t< external_lock<> > m_work_stats{ m_common_lock }; stats_collector_t< external_lock<> > m_wait_stats{ m_common_lock }; ... }; class tricky_dispatcher_t { ... private : // Для случая, когда должен использоваться внешний lock-объект // какого-то другого типа. mpmc_queue_traits::lock_t m_common_lock; stats_collector_t< external_lock< mpmc_queue_traits::lock_t > > m_work_stats{ m_common_lock }; stats_collector_t< external_lock< mpmc_queue_traits::lock_t > > m_wait_stats{ m_common_lock }; ... }; |
Но, как оказалось, все это были цветочки. Ягодки пошли когда оказалось, что в некоторых случаях в методах start() и stop() нельзя захватывать lock-объект, т.к. эти методы вызываются в контексте, где внешний lock-объект уже захвачен.
Первая мысль была в том, чтобы сделать пары методов start_no_lock()/start() и stop_no_lock()/stop(). Но эта идея не очень хороша. В частности, такое деление может затрудить использование stats_collector-а в каком-нибудь шаблоне. В коде шаблона может быть непонятно, должен ли вызываться start_no_lock() или же просто start(). Да и вообще наличие start_no_lock() вместе со start() выглядит некрасиво и затрудняет использование stats_collector-а.
Поэтому поведение шаблона stats_collector_t было изменено:
template< typename LOCK_HOLDER > class stats_collector_t { using start_stop_lock_t = typename LOCK_HOLDER::start_stop_lock_t; using take_stats_lock_t = typename LOCK_HOLDER::take_stats_lock_t; public : ... void start() { start_stop_lock_t lock{ m_lock_holder }; ... } void stop() { start_stop_lock_t lock{ m_lock_holder }; ... } so_5::stats::activity_stats_t take_stats() { ... { take_stats_lock_t lock{ m_lock_holder }; ... } ... } ... }; |
Теперь тип LOCK_HOLDER должен определить два имени типа: start_stop_lock_t (как блокировка выполняется в методах start() и stop()) и take_stats_lock_t (как блокировка выполняется в методе take_stats()). А уже класс stats_collector_t и их помощью делает или не делает блокировку lock-объекта у себя в коде.
Простой класс internal_lock определяет эти имена тривиальным образом:
class internal_lock { traits::lock_t m_lock; public : using start_stop_lock_t = std::lock_guard< internal_lock >; using take_stats_lock_t = std::lock_guard< internal_lock >; internal_lock() {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } }; |
А вот шаблон external_lock потребовалось расширить и добавить еще один параметр -- политику блокировки:
template< typename LOCK_TYPE = activity_traits::lock_t, template<class> class LOCK_POLICY = default_lock_policy > class external_lock { LOCK_TYPE & m_lock; public : using start_stop_lock_t = typename LOCK_POLICY< external_lock >::start_stop_lock_t; using take_stats_lock_t = typename LOCK_POLICY< external_lock >::take_stats_lock_t; external_lock( LOCK_TYPE & lock ) : m_lock( lock ) {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } }; |
Ну и реализация классов для политик блокировки выглядит так:
template< typename L > struct no_actual_lock { no_actual_lock( L & ) {} /* Принипиально ничего не делаем */ }; template< typename LOCK_HOLDER > struct default_lock_policy { using start_stop_lock_t = std::lock_guard< LOCK_HOLDER >; using take_stats_lock_t = std::lock_guard< LOCK_HOLDER >; }; template< typename LOCK_HOLDER > struct no_lock_at_start_stop_policy { using start_stop_lock_t = no_actual_lock< LOCK_HOLDER >; using take_stats_lock_t = std::lock_guard< LOCK_HOLDER >; } |
Получается, что в случае default_lock_policy в качестве start_stop_lock_t выступают классы std::lock_guard и в методах start()/stop() происходит реальная блокировка lock-объектов. А вот когда используется политика no_lock_at_start_stop_policy, то start_stop_lock_t -- это пустой тип no_actual_lock, который ничего не делает ни в конструкторе, ни в деструкторе. Поэтому блокировки в start()/stop() нет. Да и сам экземпляр start_stop_lock_t (он же no_actual_lock) скорее всего будет просто выброшен оптимизирующим компилятором.
Ну а использование stats_collector_t в разных случаях стало выглядет вот так:
class one_dispatcher_t { ... private : // Для случая, когда должен использоваться внешний lock-объект. activity_traits::lock_t m_common_lock; stats_collector_t< external_lock<> > m_work_stats{ m_common_lock }; stats_collector_t< external_lock<> > m_wait_stats{ m_common_lock }; ... }; class tricky_dispatcher_t { ... private : // Для случая, когда должен использоваться внешний lock-объект // какого-то другого типа. mpmc_queue_traits::lock_t m_common_lock; stats_collector_t< external_lock< mpmc_queue_traits::lock_t > > m_work_stats{ m_common_lock }; stats_collector_t< external_lock< mpmc_queue_traits::lock_t > > m_wait_stats{ m_common_lock }; ... }; class very_tricky_dispatcher_t { ... private : // Для случая, когда должен использоваться внешний lock-объект // какого-то другого типа, да еще и захватывать его в операциях // start() и stop() не нужно. complex_event_queue_t::lock_t m_common_lock; stats_collector_t< external_lock< complex_event_queue_t::lock_t, no_lock_at_start_stop_policy > > m_wait_stats{ m_common_lock }; ... }; |
Ну вот как-то так пока. Но код, для которого все это понадобилось, пока еще в стадии активной разработки, так что не исключено, что stats_collector_t и сопутствующие ему классы усложняться еще больше.
Комментариев нет:
Отправить комментарий