четверг, 18 сентября 2014 г.

[prog.c++] Версия 1.0.0 библиотеки timertt

Теоретические изыскания, начатые в двух заметках про таймеры (#1, #2) завершились вот таким практическим результатом -- на SourceForge выложен первый релиз библиотеки timertt.

Библиотека timertt -- это header-only библиотека, не имеющая внешних зависимостей, базирующаяся только на стандартной библиотеке C++11 (правда, нужно, чтобы там была корректная реализация std::chrono::steady_clock, иначе будут глюки при коррекции локального времени). Должна быть кросс-платформенной, т.к. никаких системно-зависимых вещей в ней нет. Я ее проверял под Visual C++ 2013 (Windows) и GCC 4.9.1 (Windows, Linux).

Лицензия: 3-х секционная BSD. Т.е. использоваться может без проблем как в открытых, так и в закрытых проектах.

Библиотека поддерживает только таймеры на основе тайм-аутов, т.е. таймеры, которые должны сработать через сколько-то миллисекунд (секунд, минут и т.д.) после момента активации таймера. wallclock-таймеры не поддерживаются.

Таймеры могут быть однократными (срабатывают всего один раз, после чего деактивируются), либо периодическими (повторяются до тех пор, пока не будут явно деактивированы или пока таймерная нить не завершит свою работу).

Библиотека поддерживает три таймерных механизма: timer_wheel, timer_heap и timer_list, у каждого из которых есть свои преимущества и недостатки. Может поддерживаться большое количество таймеров (сотни тысяч, миллионы или даже десятки миллионов) и обеспечивается высокая скорость обработки таймеров (до нескольких миллионов в секунду, но это зависит от времени работы связанных с таймером пользовательских событий).

В коде все это выглядит приблизительно следующим образом:

#include <iostream>
#include <cstdlib>

#include <timertt/all.hpp>

using namespace std;
using namespace std::chrono;
using namespace timertt;

int main()
{
   timer_wheel_thread_t tt;

   // Timer thread must be started before activation of timers.
   tt.start();

   // The simple single-shot timer.
   tt.activate( milliseconds( 20 ),
         []() { cout << "Simple one-shot" << endl; } );

   // The simple periodic timer.
   // Will work until timer thread finished.
   tt.activate( milliseconds( 20 ), milliseconds( 20 ),
         []() {
            static int i = 0;
            cout << "Simple periodic (" << i << ")" << endl;
            ++i;
         } );

   // Allocation of timer and explicit activation.
   auto id1 = tt.allocate();
   tt.activate( id1, milliseconds( 30 ),
         []() {
            cout << "Preallocated single-shot timer" << endl;
         } );

   // Periodic timer with timer preallocation, explicit activation
   // and deactivation from the timer action.
   auto id2 = tt.allocate();
   tt.activate( id2, milliseconds( 40 ), milliseconds( 15 ),
         [id2, &tt]() {
            static int i = 0;
            cout << "Preallicated periodic (" << i << ")" << endl;
            ++i;
            if( i > 2 )
               tt.deactivate( id2 );
         } );

   // Single-shot timer with explicit activation and deactivation
   // before timer event.
   auto id3 = tt.allocate();
   tt.activate( id3, milliseconds( 50 ),
         []() {
            cerr << "This timer must not be called!" << endl;
            std::abort();
         } );
   tt.deactivate( id3 );

   // Wait for some time.
   this_thread::sleep_for( milliseconds( 200 ) );

   // Finish the timer thread.
   tt.shutdown_and_join();
}

Библиотека разрабатывалась для замены ACE в проекте SObjectizer, поэтому все, что связано с timertt, находится на SourceForge:

  • архивы с исходными текстами доступны в секции Files. Архив timertt-1.0.0-headeronly.7z содержит только основной заголовочный файл со всей функциональностью timertt. Архив timertt-1.0.0-full.7z содержит так же тесты, примеры и сгенерированный посредством Doxygen API Reference Manual;
  • основная документация для проекта собрана в Wiki. На данный момент она на русском языке, поскольку так было быстрее. Но потихонечку документацию буду переводить на английский. Если кто-то вызовется помочь с переводом, буду очень признателен, т.к. у меня это займет много времени, а результат будет весьма так себе;
  • исходники лежат в Subversion-репозитории на SourceForge. Релизные версии в tags/timertt, находящиеся в разработке версии в branches/timertt. В Svn на SF, а не в git-е на GitHub-е потому, что мне timertt нужна в SObjectizer-е, и в SObjectizer уже давно используется подключение зависимостей через svn:externals.

Это самая первая версия, которая должна была быть сделана максимально быстро. Поэтому, вероятно, есть какие-то огрехи и просчеты, как в архитектуре/дизайне, так и в реализации. В случае, если к timertt будет какой-то интерес вне SObjectizer-а, можно будет попробовать развивать ее дальше.

Например, я не делал общего класса-интерфейса с методами start/shutdown/allocate/activate/deactivate и не наследовал от него классы таймерных нитей. Сделано это было потому, что методы activate являются шаблонными (т.к. классы для представления временных интервалов в std::chrono сами являются шаблонными). Что позволяет при вызове activate указывать разные единицы измерения:

tt.activate(
  // Задержка в 15 секунд...
  std::chrono::seconds(15),
  // После чего повтор каждые 2 минуты.
  std::chrono::minutes(2),
  []() { ... } );

В принципе, это может быть не самым лучшим решением, т.к., если пользователю потребуется инкапсулировать у себя детали работы таймера, выставив наружу унифицированный интерфейс, то это потребует от пользователя создания какой-то собственной инфраструктуры. С другой стороны, пользователю проще, т.к. он может жестко зафиксировать у себя какие-то единицы измерения в качестве базовых (например, миллисекунды).

Еще одно жестко принятое решение -- это представление таймерных событий в виде экземпляров std::function. Можно допустить, что где-то это будет накладно и неудобно. Возможно, тип таймерного события имеет смысл сделать параметром шаблона таймерной нити. Это вполне обсуждаемо. Но нужно, чтобы это было действительно кому-то нужно :) На абстрактные рассуждения "а вот так было бы хорошо и интересно...", к сожалению (или к счастью) нет времени, думать некогда, трясти нужно :)

Еще одна вещь, которую можно было бы покурить и, наверно, для каких-то приложений это могло бы быть актуально: сейчас таймеры создаются посредством метода allocate() таймерной нити (этот метод предварительно явно вызывается пользователем, либо неявно самой таймерной нитью внутри activate()). Создаются как динамические объекты, время жизни которых контролируется посредством интрузивных умных указателей. Однако, вполне могу допустить, что где-то может быть выгодно, чтобы пользователь сам создавал объекты таймеры и отвечал за время их жизни. Тогда работа с таймерами выглядела бы вот так:

timer_wheel_thread_t::timer_t timer;
tt.init( timer );
tt.activate( timer, ... );

Выгода этого подхода в том, что таймерные нити вполне могут работать без динамического выделения и освобождения памяти. Особенно такие механизмы, как timer_wheel и timer_list (там все делается посредством провязывания ссылок внутри уже созданных объектов-таймеров). Т.е., timertt можно было бы использовать и в ситуациях, когда работа с динамической памятью нежелательна.

В общем, это самая первая работающая версия, которая покрывает мои личные нужды (могу похвастаться тем, что версия ядра SObjectizer без ACE есть и вполне хорошо себя чувствует). Возможности для дальнейшего развития timertt наверняка имеются. Но для этого развития нужна не только моя заинтересованность :)

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