LINUX.ORG.RU

Обёртка над Posix Timers

 ,


0

3

Написал для своего небольшого проекта обёртку над таймерами Posix.

class VirtualTimer {
	private:
		pthread_mutex_t _mutex;
		
		timer_t _posixTimer;
		
		uint32_t _period;
		
		TimerCallback _callback;
		
		void *_callbackArg;
		
		volatile bool _armed;
		
		static void posixTimerCallback(union sigval val);
		
	public:
		VirtualTimer(uint32_t period = 0, TimerCallback callback = NULL, void *arg = NULL);
		
		~VirtualTimer();
		
		void setCallback(TimerCallback callback, void *arg);
		
		bool setPeriod(uint32_t period);
		
		uint32_t getPeriod() {
			return _period;
		}
		
		void start();
		
		void stop();
		
		bool isArmed() {
			return _armed;
		}
		
};

VirtualTimer::VirtualTimer(uint32_t period, TimerCallback callback, void *arg) {
	_period = period;
	_callback = callback;
	_callbackArg = arg;
	_armed = false;
	pthread_mutex_init(&(_mutex), NULL);
	struct sigevent sev;
	sev.sigev_notify = SIGEV_THREAD;
	sev.sigev_notify_function = posixTimerCallback;
	sev.sigev_value.sival_ptr = this;
	int r = timer_create(CLOCK_MONOTONIC, &sev, &(_posixTimer));
	assert(r == 0);
	if (_period > 0) {
		start();
	}
}

VirtualTimer::~VirtualTimer() {
	timer_delete(_posixTimer);
	pthread_mutex_lock(&(_mutex));	
	pthread_mutex_destroy(&(_mutex));
}

void VirtualTimer::posixTimerCallback(union sigval val) {
	VirtualTimer *timer = (VirtualTimer*)val.sival_ptr;
	pthread_mutex_lock(&(timer->_mutex));
	if ((timer->_callback != NULL) && (!timer->_callback(timer->_callbackArg))) {
		timer->stop();
	}
	pthread_mutex_unlock(&(timer->_mutex));
}

void VirtualTimer::setCallback(TimerCallback callback, void *arg) {
	_callback = callback;
	_callbackArg = arg;
}

bool VirtualTimer::setPeriod(uint32_t period) {
	_period = period;
	if (_armed) {
		start();
	}
	return true;
}

void VirtualTimer::start() {
	time_t sec = _period / 1000000;
	long nsec = (_period % 1000000) * 1000;
	struct itimerspec newValue = {
		.it_interval = {
			.tv_sec = sec,
			.tv_nsec = nsec
		},
		.it_value = {
			.tv_sec = sec,
			.tv_nsec = nsec
		}
	};
	int r = timer_settime(_posixTimer, 0, &newValue, NULL);
	assert(r == 0);
	_armed = true;
}

void VirtualTimer::stop() {
	struct itimerspec newValue = {};
	int r = timer_settime(_posixTimer, 0, &newValue, NULL);
	assert(r == 0);
	_armed = false;
}

Имеется проблема. Допустим, таймер срабатывает и вызывается callback. Однако в этот же момент поток, создавший таймер, решает, что хватит дожидаться его срабатывания и уничтожает объект. В итоге callback (с самого начала или на своей середине) будет работать с уничтоженным объектом и это может привести к печальным последствиям.

Сейчас это частично решается с помощью mutex. Если callback успеет захватить mutex, то деструктор будет терпеливо ждать, пока callback отработает (новый callback уже точно не вызовется, потому что таймер дизармится перед захватом mutex) и только потом уничтожит таймер. Однако если нет? Если деструктор успеет захватить mutex, затем будет вызван callback (callback уже может быть вызван, но таймерный поток ожидает своего кванта времени в очереди планировщика ОС, пока работает деструктор), который будет ждать mutex, который потом уничтожат (что уже само по себе будет плохо, и либо будет зависание таймерного потока, либо обращение к освобождённой области памяти)? Будет плохо.

Как эту ситуацию разрулить?

★★★★★

Последнее исправление: beastie (всего исправлений: 4)

pthread_mutex_lock(&(_mutex));	
pthread_mutex_destroy(&(_mutex));

Дзен-деструктор.

Как эту ситуацию разрулить?

Другим дизайном.

tailgunner ★★★★★
()

Возможно стоит подумать в сторону делегирования реального удаления обьекта коллбэку, в случае, если таймер взведён (передача во владение pimpl обьекта)?

Коллбэк перед отработкой проверяет - отпустил ли владелец реализацию, если отпустил - удаляет реализацию и выходит.

Цена вопроса - таймеры всегда живут в куче. Возможные проблемы: таймер можно удалить из вне зная его id.

pon4ik ★★★★★
()

Учитвая, что коллбэк всё равно зовётся из другого потока - не проще ли просто использовать кондиционную переменную со сном по таймауту?

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

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

zaz ★★★★
()

Как эту ситуацию разрулить?

Например, enable_shared_from_this/shared_from_this. Или явным подсчётом ссылок. Или сделать бесконечное время жизни класса.

i-rinat ★★★★★
()

Можно положить в кучу весь таймер или его приватный контекст, как выше уже подсказали. Или можно сделать в духе стандартной либы: сказать, что вызов деструктора пока isArmed — UB. Кстати, у тебя data race между posixTimerCallback и всеми остальными функциями.

const86 ★★★★★
()

Пару лет назад, когда делал библиотеку для работы с таймерами вышел из ситуации с помощью двух вещей:

1. Подсчет ссылок на таймерные заявки. Заявка уничтожалась когда и ее первоначальный владелец и таймерная нить удалили все ссылки на заявку у себя.

2. Добавлением для каждой таймерной заявки статуса. Так, был специальный статус wait_for_execution, который означал, что данная заявка сейчас либо ждет запуска на таймерной нити, либо обрабатывается в данный момент. И поэтому ее нельзя удалять прямо сейчас. В этой ситуации статус менялся на wait_for_deactivation и таймерная нить удаляла заявку сама после того, как обработает ее.

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

Радикально изменил дизайн. Теперь использую только один posix timer. У меня есть список виртуальных таймеров, отсортированный по срочности (у каждого таймера рассчитано в какое время он должен сработать). start добавляет таймер в список, stop удаляет. Если первый элемент в списке таймеров изменился (а это всегда будет самый срочный таймер), то изменяем параметры posix timer, чтобы он сработал тогда, когда этому таймеру придёт время срабатывать (если список пуст, то дизармим posix timer). posix timer же вызывает функцию tick, которая находит в списке все таймеры, которым пришло время сработать, и удаляет их, а затем вызывает callback (именно в таком порядке, чтобы если мы захотим рестартовать таймер после срабатывания, то его уже не было в списке на момент вызова doStart).

Главная фишка в том, что start, stop и tick на время своей работы захватывают общий mutex. Таким образом если уже запустился tick, то stop не сможет начать работу, пока все callback не отработают. А если tick будет вызван в середине stop, то он подождёт, пока таймер будет удалён из списка. В итоге он уже не сможет вызвать его callback, когда сможет продолжить работу (ведь таймера в списке не будет).

http://pastebin.com/BXvkhPBC

lockSystem, unlockSystem - захватывают и освобождают общий mutex соответственно.

virtualTimerDeadlineChanged - перенастраивает posix timer сработать в абсолютное время newDeadline (микросекунды). Если время равно TIME_INFINITE, то дизармит posix timer (но не уничтожает его).

getCurrentTimestamp - возвращает текущее время в микросекундах.

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

Ну я и так считаю, что с VirtualTimer в один момент времени должен работать только один поток. Это в принципе логично. Но вот убедиться, что callback точно не будет вызван в момент уничтожения, программисту будет проблематично. Ведь это во многом зависит от текущего времени и планировщика ОС (таймерный поток, который уже собирается вызвать callback, может быть отложен на неопределённый срок). И isArmed отражает мнение класса, но не подсистемы posix timers. А проверить его в callback, если объект уже уничтожен, нельзя.

В общем, я сильно переработал дизайн всего - Обёртка над Posix Timers (комментарий). Теперь вроде как состояние гонки должно всегда разруливаться с помощью mutex.

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

Меня в твоём коде расстраивают подчёркивания в начале имени, указатели типа void, явные си-стайл преобразования типов, лок-анлок отдельными функциями и volatile.

i-rinat ★★★★★
()
Ответ на: комментарий от KivApple

Радикально изменил дизайн.

Радикальное изменение дизайна - это, например, переход на eventfd вместо коллбеков.

А ты, кажется, изобретаешь timer wheel. Сознательно?

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

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

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

И таки да, вы пытаетесь переизобрести timer wheel.

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

Явное преобразование типов закралось в код случайно в прошлом. Если его убрать (что я только что и сделал для повышения читаемости), то ничего не изменится. Указатели типа void применяются только для аргумента callback, в чём я не вижу ничего плохого. А как избавиться от volatile?

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

Да, сознательно. Вообще это кусок моей RTOS под микроконтроллеры. Просто добавил для полноты и для тестирования поддержку Linux (при этом вместо реального планировщика задач обёртка над pthread). На голом железе всё было проще, потому что в критических местах были по той или иной причине запрещены прерывания (в функциях start и stop явно, в функции tick потому что она выполняется в обработчике прерывания аппаратного таймера).

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

Как я понимаю, если разрешить рекурсивный захват mutex, проблема решиться? По идее функция tick нормально отреагирует, если внутри callback будет вызван start или stop для любого таймера.

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

Как я понимаю, если разрешить рекурсивный захват mutex, проблема решиться?

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

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

stop не вызывает освобождение памяти из-под таймера. Он просто удаляет таймер из списка активных. start может добавить его обратно. Двойной stop не имеет эффекта, как и двойной start, ибо есть проверки _armed. Запретить вызывать деструктор таймера из callback я считаю вполне нормальным. Объекты должны уничтожаться в тех потоках, которые ими владеют.

KivApple ★★★★★
() автор топика
Последнее исправление: KivApple (всего исправлений: 1)

Автору стоит сменить свой ник на что-нибудь типа KostylVelosipedov

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