LINUX.ORG.RU

timertt — библиотека с реализацией таймерных нитей для C++11

 


5

2

Дабы выбросить из своего проекта ACE Framework пришлось сделать свою реализацию таймеров. Получилась небольшая библиотека, которая не имеет внешних зависимостей и использует только возможности стандартной библиотеки C++11. Проверялась под Windows (MSVC++2013, MinGW-w64 GCC 4.9.1) и Linux (GCC 4.9.1).

Лицензия: 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 << "Preallocated 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();
}

Скачать можно с SourceForge: только header-only вариант или же полный вариант с тестами/примерами. Документация там же в Wiki (пока на русском языке, потихоньку будет переводиться на английский).

Еще чуть-чуть подробностей по релизу здесь.

Сразу поясню для желающих спрашивать «нафига это нада?» и/или «афтар, а чем это лучше/хуже?». Если вы в своем проекте уже используете какой-то фреймворк/библиотеку, предоставляющий таймеры (например, ACE/Boost/Qt/wxWidgets/libuv/libev/libevent/you-name-it), то, скорее всего, timertt вам не нужен. Если только вы не обнаружите, что ваш инструмент не очень хорошо справляется с миллионом таймеров или же вам надоело натягивать свою прикладную логику на API вашего инструмента (актуально, например, для ACE, где таймерные очереди реализованы здорово, на вот API для них несколько своеобразный и не всегда удобный).

Если же в вашем проекте никаких тяжеловесных зависимостей нет, а таймеры нужны, то можно и в сторону timertt посмотреть.

Ну а вообще делал для себя, но не вижу причин не выложить в виде OpenSource.

★★★★★
Ответ на: комментарий от eao197

Это конечно, злостный оффтопик, но если уже есть пайпы, то почему для работы с сокетами нужно boost::asio тянуть, а не работать напрямую с асинхронным вводом-выводом Win32?

Так и пришлось - юзать WinSock напрямую, вместо на порядок более удобного boost::asio.

Pavval ★★★★★
()
Ответ на: комментарий от anonymous

Вот пример, который надо ускорить (корявый, но пускабельный):

У меня именно этот тест отработал за 4m59s.

А замена wheel на list уменьшило время работы до 4m31s (правда версия не из релиза, а из бранча, там make_exec_list чуть модифицирован).

Так что timer_list имело смысл сделать хотя бы ради таких бенчмарков :)

Сильно подозреваю, что все упирается в mutex, который постоянно лочится на activate. Если его заменить на простейший spinlock, то можно еще больше ускориться. Правда, у авторитетных тимлидеров это одобрения не вызовет :)))

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Правда, у авторитетных тимлидеров это одобрения не вызовет

замени и проверь. разница будет в N пар инструкций call/ret

anonymous
()
Ответ на: комментарий от anonymous

См. например https://github.com/berkus/dir_monitor/blob/master/include/inotify/basic_dir_m...

Это тупое использование boost::asio::posix::stream_descriptor, а не принципиально новый(с) service.

Но да, частично посыпаю голову пеплом: Я не знал про windows::stream_handle и posix::stream_descriptor - по ходу их не было в той версии буста, что я раньше использовал. Остается вопрос про подключение HANDLE от примитивов синхронизации.

Pavval ★★★★★
()
Ответ на: комментарий от Pavval

Это тупое использование boost::asio::posix::stream_descriptor, а не принципиально новый(с) service.

Определение «принципиально нового» в студию. В Asio нет поддержки асинхронных файловых операций. Пример данного сервиса показывает как он добавляется в _существущий_ event loop.

Конечно это пример тупого использования stream_descriptor, ведь кодерЪ не пользуется гуглом. Иначе бы он знал, что в интернете кроме документации есть еще и примеры по которым школьник может наваять свой сервис:

http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/example/cpp03/servic...

https://github.com/BoostGSoC13/boost.afio http://evgeny-lazin.blogspot.ru/2009/02/boostasio.html

anonymous
()
Ответ на: комментарий от anonymous

Правильно ли я понял, что суть сервисов тупо в том, что они, занимая отдельный поток под ожидание, тупо в конце концов делают io_service::post()?

Pavval ★★★★★
()
Ответ на: комментарий от Pavval

Естественно, если какой-то API не поддерживает асинхронные операции, то и нет никакого волшебства - приходится асинхронность тупо имитировать в тупо отдельном потоке. Вероятно, такова судьба у модных в последнее время event-based либ, в которых школьники увидели очередную серебряную пулю.

Суть boost::asio::io_service::service тупо в том, чтобы расширить функционал io_service не городя костыли в виде цикла собственного цикла на io_service::poll (хотя последнее и не возбраняется)

Так что внезапно автор boost.asio не последний пидор, и привет в timertt — библиотека с реализацией таймерных нитей для C++11 (комментарий) летит мимо тазика.

anonymous
()
23 ноября 2014 г.
Ответ на: комментарий от Pavval

Pavval

Зопили безпоточную реализацию с возможностью встраивания в event loop посредством classname::get_read_fd()+classname::process_timers(). А тогда уже сделай поток с простейшим select() внутри - для тех кому покатит и в отдельном потоке.

staseg

UPD. И про event-loop тут все правильно говорят.

Нет ли желания обсудить, как должен выглядеть API однопоточной версии? Сейчас у меня такая версия есть, работает. Но вот ее API делался под мою задачу. Сильно сомневаюсь, что он под интеграцию с абстрактным event-loop подойдет.

Однако, если у кого-то есть конкретные соображения о том, какой API нужен, то можно над этим поработать.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

Как пример погляди http://qt-project.org/doc/qt-4.8/qsocketnotifier.html

Тут почти то же самое:

*.h:

struct platform_handle_t;

class AsyncProcessor
{
  virtual platform_handle_t* GetEvent() = 0;
  virtual void ProcessEvent() = 0;
};

class MyTimer : public AsyncProcessor
{
  virtual platform_handle_t* GetEvent() override;
  virtual void ProcessEvent() override;
 ...
};

*.cpp:

#ifdef _WIN32

struct platform_handle_t
{
  HANDLE handle;
};

#else // _WIN32
#ifdef _POSIX_VERSION

struct platform_handle_t
{
  int fd;
  int flags;
};

#else // _POSIX_VERSION

#error Unsupported OS, dude

#endif _POSIX_VERSION
#endif // _WIN32

Pavval ★★★★★
()
Ответ на: комментарий от Pavval

Так а суть этого всего в чем? Чтобы был какой-то платформенно-зависимый дескриптор, который бы можно было передать, например, в select или WaitForMultipleObjects. И который бы взводился, когда приходит время обработки какого-то из таймеров?

eao197 ★★★★★
() автор топика

Забавно. Не так давно в какой-то своей поделке пришлось накостылить частный случай вот этого :]

intelfx ★★★★★
()
Ответ на: комментарий от eao197

Ну, если я правильно понял цель этой либы...

У меня создаётся отдельный поток, которому через очередь передаётся последовательность «событий» (таймстамп+длина импульса). Он их читает, ждёт наступления ближайшего, пинает микроконтроллер (собственно генерируя фронт импульса), спит указанное время и повторяет. Ещё наложения событий обрабатываются.

Как-то так: https://github.com/intelfx/pulse-dispatcher/blob/master/pulse_worker.cpp#L28

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 2)
Ответ на: комментарий от intelfx

Я так понял, у вас нет возможности изымать импульсы из середины очереди. А добавление идет всегда в конец. Правильно?

eao197 ★★★★★
() автор топика
Ответ на: комментарий от intelfx

Ну это очень простой сценарий :) Когда появляется необходимость изымать таймеры и/или добавлять в середину списка, вот тогда самая веселуха начинается :)

eao197 ★★★★★
() автор топика
Ответ на: комментарий от intelfx

Просто здесь много чего обсуждалось, поэтому решил уточнить о чем именно речь.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

В целом да. Пара «событие-обработчик» - это именно то, что можно встроить в event loop. По какому поводу срабатывает событие и что в обработчике - внутренние детали реализации. Так или иначе, код коллбека таймера вызывается только из реализации обработчика. (а вот тут не забываем про рекурсивную работу с объектом таймера, т.к. обработчик может выставить новый таймер).

Pavval ★★★★★
()
Ответ на: комментарий от Pavval

Ну тогда, если приложение должно быть однопоточным, возникает два вопроса:

1. Откуда брать источник времени? Т.е. кто и как будет контролировать что вот случился момент Ч и можно обработать пачку таймеров с минимальным временем срабатывания.

2. На каком контексте будет происходить активация дескриптора для того, чтобы на основной нити произошел возврат из select-а?

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

Поэтому сейчас у меня сделан другой подход. Объект timer_manager может вернуть время срабатывания ближайшего таймера. Это время можно передать в качестве времени ожидания в select. После чего следует вызов select-а со списком реальных дескрипторов, на которых вы ждете событий. Возврат из select-а произойдет либо при появлении какого-то события на каком-то дескрипторе, либо по истечении тайм-аута. Если истек тайм-аут, значит можно дернуть timer_manager.process_expired_timers(), в результате чего отработают обработчики истекших таймеров.

Все делается на контексте одно единственной нити. Никаких примитивов синхронизации не нужно.

eao197 ★★★★★
() автор топика
Ответ на: комментарий от eao197

На оба вопроса:

Под виндой через timeSetEvent() в главном же треде. Под unix - как всегда через ж*пу. Сигналы можно было бы заюзать для выставления события, но они не подходят для реализации как либы. Ну т.е. пока кроме второго потока (которого да, я так не хотел) я не вижу.

Поэтому сейчас у меня сделан другой подход.

Интересный подход. Любой event loop имеет возможность реализовывать таймер. Но тогда таймеры наиболее уместно реализовывать именно как часть цикла, а не отдельно.

Pavval ★★★★★
()
Ответ на: комментарий от Pavval

Интересный подход. Любой event loop имеет возможность реализовывать таймер. Но тогда таймеры наиболее уместно реализовывать именно как часть цикла, а не отдельно.

Ну как бы не любой :)

Например, я сейчас собираюсь задействовать libmosquittopp (это клиент для MQTT протокола). Там общение с сокетами внутри спрятано, наружу выставлен метод loop, в который можно передать максимальное время нахождения внутри loop-а. В моем случае получается приблизительно такая схема работы:

timer_manager tm(...);
app_event_queue eq(...);
mosqpp::mosquittopp mosq(...);
...
while(true)
{
  // Process application events while they exist...
  while( !eq.empty() )
  {
    process_app_event( eq.pop() );
    // Process all expired timers.
    tm.process_expired_timers();
  }

  // Process any MQTT-related events.
  auto r = mosq.loop(
    // Wait no more time than next timer event.
    to_milliseconds( tm.timeout_before_nearest_timer( millisecons(1000) ) ) );
  if( r != MOSQ_ERR_SUCCESS )
    ... // Some error handling.
}

Тут все должно работать, поскольку прикладные события возникают только при обработке прочитанных из MQTT сообщений.

Если же приложению нужен более низкоуровневый event-loop, то надобность в timertt может возникнуть только если в приложении не используются инструменты вроде libev/libevent/libuv/asio/ace, где таймеры свои собственные.

eao197 ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.