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

Он не пустой,

Я про деструктор.

Упс, что-то я не знаю, видимо, про идеологию C++

Идеология С++ - ты не обязан платить за то, что не используешь.

По поводу необходимости ERROR_LOGGER-а и ACTOR_EXCEPTION_HANDLER-а подробности вот здесь (см. так же пример).

В том то и дело, что нет такой необходимости. Ты придумал лишнюю сущность. Исключения не должны кидаться в «пустоту», и уж тем более не должно быть единой большой функции, которая будет обрабатывать все-все исключения.

Был бы в C++11/14 штатный шаблон интрузивного умного указателя, свой бы делать не пришлось (а shared_ptr — это неинтрузивный указатель, дополнительные накладные расходы).

А ты замерял разницу? Сколько ты реально выиграл и выиграл ли хоть что-то существенное? Зато ты реально и очевидно проиграл по простоте и объему кода.

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

Я про деструктор.

И что в этом плохого?

В том то и дело, что нет такой необходимости.

Не нужно мне сказки рассказывать. В том проекте, под который я делал timertt, я задаю кастомные error_logger и exception_handler-ы. За счет параметров шаблона за это не нужно ничего платить.

Исключения не должны кидаться в «пустоту», и уж тем более не должно быть единой большой функции, которая будет обрабатывать все-все исключения.

Если пользователь не выпускает наружу исключений из своих таймерных событий, то вообще никаких проблем нет. Но если ошибается и выпускает, что делать библиотеке?

Сколько ты реально выиграл и выиграл ли хоть что-то существенное?

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

В случае интрузивного умного указателя этого нет вообще. От слова совсем.

Зато ты реально и очевидно проиграл по простоте и объему кода.

Пользователю библиотеки от этого хуже стало? Какая ему разница, является ли timer_holder_t собственной реализацией интрузивного умного указателя или это синоним для std::shared_ptr?

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

Сколько ты реально выиграл и выиграл ли хоть что-то существенное?

Кроме того, ЕМНИП, с std::shared_ptr было бы больше мороки с организацией хранения таймерных заявок в таймерных механизмах. Сейчас хранятся простые указатели, счетчик ссылок в самом объекте, ничего лишнего, операции перемещения указателей происходят очень эффективно. В случае с std::shared_ptr пришлось бы во внутренних структурах хранить std::shared_ptr-ы и платить за это инкрементами/декрементами при перестройке структур.

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

И что в этом плохого?

Ничего, как и в ненужных inline, например, просто лишний код.

В том проекте, под который я делал timertt, я задаю кастомные error_logger и exception_handler-ы.

Я и не сомневался, иначе бы ты эту логику не втулил бы.

В случае интрузивного умного указателя этого нет вообще. От слова совсем.

Не стоит объяснять разницу, она общеизвестна, но ты так и не ответил на вопрос - дает ли это _реальный_ выигрыш. Или только если создавать миллионы бесполезных таймеров в секунду, то может вылезет процент-другой «оверхеда»?

Пользователю библиотеки от этого хуже стало?

Так можно все что угодно в деталях реализации оправдать :)

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

И что в этом плохого?

Ничего, как и в ненужных inline, например, просто лишний код.

Тогда в чем проблемы? Ну вот привык писать inline там, где функция/метод по замыслу автора должна быть inline. Это вызывает у вас эстетическое неприятие? Тогда в сад.

Я и не сомневался, иначе бы ты эту логику не втулил бы.

Ну и? Если мне это потребовалось в одном проекте, почему это не может не потребоваться в другом? И если не делать таким образом, то как нужно делать?

но ты так и не ответил на вопрос - дает ли это _реальный_ выигрыш.

В этом проекте не мерял. Были замеры несколько лет назад в другом проекте. Несколько процентов выигрыша интрузивные умные указатели давали. Плюс они дают еще дополнительные преимущества.

Тут вот даже товарищ Александреску озадачился оптимизацией shared_ptr для случая, когда имеется всего одна ссылка на динамически созданный объект.

Так можно все что угодно в деталях реализации оправдать :)

Доколупаться можно и к идентификатору inline, и что от этого? :)

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

Ну вот привык писать inline там, где функция/метод по замыслу автора должна быть inline. Это вызывает у вас эстетическое неприятие? Тогда в сад.

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

Ну и? Если мне это потребовалось в одном проекте, почему это не может не потребоваться в другом?

Конечно может, и не только это.

Были замеры несколько лет назад в другом проекте. Несколько процентов выигрыша интрузивные умные указатели давали

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

и что от этого? :)

Ну а что еще делать анонимусу? ;)

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

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

Ну, насколько я помню, inline я начал использовать еще до того, как появились доступные мне C++компиляторы с поддержкой шаблонов. И которые не были настолько умны, чтобы инлайнить однострочные методы автоматически. Посему, если класс не шаблонный, то inline я расставляю. Если шаблонный, то нет.

Кроме того, реализация интрузивных указателей была сделана давным-давно и кочует из проекта в проект. Видимо, когда-то inline были к месту, затем остались в коде, а менялись там только имена классов.

С деструктором thread_basic_t, как мне сейчас вспомнилось, вообще получилось забавно. Сначала он не был пустым. Затем все его действия были перенесены в производные классы. А сам деструктор остался, видимо, чтобы doxygen-овский комментарий к нему был :)

Конечно может, и не только это.

Ну в библиотеке был принят компромисс по ряду моментов. Здесь подробнее про некоторые из них. Эти два параметра шаблоны сделаны потому, что были очевидны. Есть еще как минимум один менее очевидный. Но быстрее было сделать не вынося его в параметры шаблона.

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

И которые не были настолько умны, чтобы инлайнить однострочные методы автоматически

Стандарт явно указывает, что «A function defined within a class definition is an inline function. The inline specifier shall not appear on a block scope function declaration.».

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

Спасибо за цитату из стандарта. Помнится читал что-то такое :) Но привычка — это страшное дело :)

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

И если не делать таким образом, то как нужно делать?

Например, оборачивать передаваемые функции в лямбду с try/catch. Оборачивать можно перегрузкой соответствующих методов в дочернем классе, который по сути будет реализовывать то, что ты и так отдельно выписывал.

anonymous
()

опять привет... ты должен мне новые глаза

умоляю, больше никогда не пиши на крестах.

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

И это кстати будет действительно универсальным решением, в которое можно и логирование добавить, и post/pre и т.п.

anonymous
()

Что до использования односвязных списков внутри реализации timer_wheel, то там это вообще самая эффективная структура. Как на добавление, так и на обработку, так и на изъятие.

Посмотрел реализацию..редко такое говорю, но в данном случае автору просто необходимо сообщить: «это говно».

какая к чёрту производильность shared_ptr и «большое количество таймеров (сотни тысяч, миллионы или даже десятки миллионов)»..странные интерфейсы, giant lock, O(N), кривая реализация элементарно-школьных вещей и цепепе головного мозга.

единственное - codestyle вроде как выдержан и усидчиво порождены тесты..дурной teamlead будет временно доволен :-)

я уже говорил, что за списки должны медленно и мучительно убивать? так вот за реализацию make_exec_list() предварительно дважды меняют пол периодически уестествляя клиента

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

Например, оборачивать передаваемые функции в лямбду с try/catch. Оборачивать можно перегрузкой соответствующих методов в дочернем классе, который по сути будет реализовывать то, что ты и так отдельно выписывал.

И это будет проще? Для кого? Кто за дополнительную лямбду с try/catch внутри будет платить?

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

кривая реализация элементарно-школьных вещей и цепепе головного мозга.

Например, каких?

так вот за реализацию make_exec_list() предварительно дважды меняют пол периодически уестествляя клиента

Что не так с make_exec_list()? И в каком из классов?

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

Что не так с make_exec_list()? И в каком из классов?

Во всех классах он кривой тормоз by-design. Да и излишен по той-же причине :-)

MKuznetsov ★★★★★
()
Ответ на: никак от anonymous

не заслужил своим хамским поведением

Тогда, сударь, не извольте больше 3.14здеть. Считаете себя крутым проффи — покажите, что умеете.

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

И это будет проще? Для кого?

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

Кто за дополнительную лямбду с try/catch внутри будет платить?

А что там платить? Лямбда - это функтор, т.е. банальная функция + данные, данные у нас не меняются - это std::function от пользователя, т.е. в памяти будет ровно тот же набор байт, Итого «плата» - аж целый call.

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

Во всех классах он кривой тормоз by-design. Да и излишен по той-же причине :-)

Да уж, слова эксперта с мировым именем. Это все, или еще чего-нибудь в лужу перднуть сможете?

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

Этот код может полезен где-то в XP будет, да ..

А давно буст не собирается под XP?

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

покажите, что умеете

ты что-то путаешь. показать уже второй раз пытаешься ты. и второй раз — говно. в ответ на оценку твоего потока сознания начинаешь хамить.

ладно, ты не можешь судить сколько полезных советов дал в /development я. но хамить MKuznetsov... язабан.

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

Итого «плата» - аж целый call.

Хотя зависит от использования, может добавиться и оверхед от еще одной std:;function, но он опять же не стоит внимания, особенно на фоне исключений.

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

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

Давайте с начала. А то я запутался в комментариях. Оборачивать в лямбду нужно что? Обработчик события таймера? Или обработчик выпущенного из события таймера исключения?

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

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

да

А смысл в чем? Вызов action-а все равно оборачивается внутри класса timer_thread. Соответственно, вопрос в том, как пойманное исключение обработать. Пользователь как-то должен предоставить обработчик.

Сделать это можно несколькими способами (помимо того, что было сделано в библиотеке).

1. Пользователь передает соответствующий std::function в конструктор. Получается тоже самое, что в текущем варианте, но только компилятор не сможет заинлайнить обработчик исключения, если он делает какую-то тривиальную операцию, например, использует макрос ACE_ERROR() или вызов из logcxx.

2. Можно было бы сделать в timer_thread виртуальный метод, который бы вызывался внутри catch-а. Но тогда пользователю пришлось бы переопределять этот метод в каждом из вариантов timer_thread. Этот способ был бы уместен, если бы в библиотеке использовался общий интерфейсный класс:

class timer_thread_t {
public :
  virtual void activate(...) = 0;
  virtual void deactivate(...) = 0;
  ...
protected :
  virtual void on_exception( const std::exception & x ) = 0;
  ...
};

Но этот способ не был выбран из-за того, что методы activate сделаны шаблонными. А без общего базового класса, если пользователь хочет у себя иметь возможность менять таймерный механизм в зависимости от настроек, ему придется делать наследника от каждого класса таймерной нити и там повторять действия обработчика исключения.

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

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

А ты замерял разницу? Сколько ты реально выиграл и выиграл ли хоть что-то существенное?

Задал однажды примерно такой же вопрос одному неанонимному тимлиду (без году неделя, правда, - потому с зашкаливающим ЧСВ). Он съехал на ха-ха и бла-бла-бла про «оверхед, про который и так ясно, что он есть». Теперь подросшие «сейнеры» мучаются с его велосипедами... В основном выкашивая на5.1, потому что задумано было «глобально и надежно», а где-то под дедлайн начались отступления и срезания углов (и по коду это видно - где самописные рв-локи, которые пытаются быть шареными и не шареными... и ловят дедлок на сценариях, под которые все разрабатывалось, «но ни разу не проверялось» (на одном фиде все норм... А фидов у «похожего заказчика» - 89 :)), где - структура данных называющаяся «вектор», реализованная на основе... std::list. В общем, порывы к велосипедизму - эту песню не задушишь не убъешь. Разве что - вместе с порывающимися).

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

пользователь может определить один класс обработчика исключений для своих целей, а затем использовать его как параметр в любом классе таймерной нити.

ты не понимаешь зачем нужны исключения. просто не используй их.

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

Например, оборачивать передаваемые функции в лямбду с try/catch.

Раньше для этого многие двуногие безрогие велосипедили шаблониевый класс функтора с оператором для кола... Рогатые - макросами _my_try_(f())/VERIFY_CALL(f()) эмулировали.

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

ты не понимаешь зачем нужны исключения. просто не используй их.

Еще раз для тех, кто не хочет или не умеет читать:

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

Т.е. обработчик исключений нужен для случая, когда пользователь лопухнулся, выпустил наружу исключение (которое должен был поймать и обработать сам). Все, что можно в этот момент сделать — это либо залогировать ошибку и грохнуть приложение, либо дать пользователю возможность самому это сделать. Заданный пользователем обработчик исключений именно для этого и предназначен: залогировать в какой-то лог приложения и завершить работу. Либо проглотить его. Но тут уж пускай сам решает.

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

А смысл в чем?

В том, что я предложил универсальное решение, а ты все еще решаешь свою личную частную задачу. Библиотеки на то и библиотеки, что на их основе строят нечто большое и разное.

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

Пользователь как-то должен предоставить обработчик.

Ну как-как... _set_se_translator, бгг. Не тот форум.

slackwarrior ★★★★★
()
Ответ на: комментарий от eao197
list_timer_t *
	make_exec_list()
	{
// больше надо списков, хороших и разных
		list_timer_t * head = nullptr;
		list_timer_t * tail = nullptr;

// нахрен юзеру другие источники ?
// достаточно зашибенной библиотеки упендюренной в хидер
		const auto now = monotonic_clock_t::now();

// кстати это всё внутри lock
		while( m_head && now >= m_head->m_when )
		{
// а теперь будем __поэлементно__ таскать элементы
// из головы одного списка в хвост другого - умеем же
// всё-равно же lock, а так и кода побольше и к зряплате прибавка
			list_timer_t * t = m_head;
			m_head = m_head->m_next;
			if( m_head )
				m_head->m_prev = nullptr;

			if( tail )
			{
				tail->m_next = t;
				t->m_prev = tail;
				t->m_next = nullptr;
				tail = t;
			}
			else
			{
				head = tail = t;
				t->m_prev = t->m_next = nullptr;
			}
// очень важно..а то забудем куда положили и не найдём
			t->m_status = timer_status_t::wait_for_execution;
		}
// кстати m_tail очень сцуко важное поле (как и m_prev)
// позволяет удалять таймер за O(1)
// правда путём траха на других операциях, но там юзер может и подождать

		if( !m_head )
			// Now the list is empty.
			// There must not be tail anymore.
			m_tail = nullptr;
// 
// priority queue ? не, не слышал..
// 
		return head;
	}
MKuznetsov ★★★★★
()
Ответ на: комментарий от eao197

не должны выпускать наружу исключений

твои анальные боли проистекают из двух причин:

  • если «не должны» — значит «не должны». это вполне конкретным образом выражается крестовым синтаксисом.
  • обработчик события таймера должен вызываться в контексте «пользователя».

и первое, и второе — следствия кривой архитектуры.

возвращаемся к нашим баранам. чтобы бы юзабельной, твоя «библиотека» должна естественным образом встраиваться в пользовательский event-loop (или крутить свой, но таких уже более чем достаточно) и пусть он сам решает что и в каком контексте будет у него вызываться.

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

Ой спасибо за знатный высер, ой порадовали. Эксперт, мать-мать-мать.

Давайте так. Вы прочитаете вот это. Если после этого вы еще будете настаивать на своих комментариях, я покажу в чем вы неправы. Если измените свою точку зрения, то будем считать, что поторопились, не вникли в некоторые важные детали.

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

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

ОМГ, да у тебя синдром Поттеринга.

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

Это вам кажется.

С его помощью я могу, например, сделать статистику или логирование. Уже более универсально чем у тебя.

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

твои анальные боли проистекают из двух причин:

Спасибо, поржал.

обработчик события таймера должен вызываться в контексте «пользователя»

Эт вы что-то о своем. Я свои задачи решал. Решил. Кому нужно что-то другое — пожалуйста, C++ вам в руки. Сделаете, покажете.

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

ты же отвечал на мою реплику о том, что не понимаешь зачем нужны исключения

и тут же подтверждаешь это

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

Может еще и готовый код покажете?

Зачем? Идея достаточно проста, чтоб понять ее без кода.

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

Зачем? Идея достаточно проста, чтоб понять ее без кода.

Самый лучший код тот, который не написан. Пять балов.

Пишите еще.

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

Самый лучший код тот, который не написан. Пять балов.

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

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

Кому нужно что-то другое — пожалуйста, C++ вам в руки.

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

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