вторник, 23 августа 2016 г.

[prog.c++] Трансформация маленького класса от совсем простого к легкому хардкору с policy-based design

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;
               iftrue == (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.
      templatetypename... 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 так же стал шаблоном:

templatetypename 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 было изменено:

templatetypename 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<classclass 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(); }
   };

Ну и реализация классов для политик блокировки выглядит так:

templatetypename L >
struct no_actual_lock
   {
      no_actual_lock( L & ) {} /* Принипиально ничего не делаем */
   };

templatetypename 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 >;
   };

templatetypename 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 и сопутствующие ему классы усложняться еще больше.

Комментариев нет: