Здравствуйте, уважаемые читатели!
Продолжая серию постов по многопоточному программированию, хочется коснуться одной фундаментальной проблемы использования сигнальных переменных в Linux, к сожалению, не имеющей пока красивого универсального решения (а может оно просто неизвестно мне). Многие, к сожалению, даже не догадываются о том, что такая проблема имеет место быть.
Рассмотрим простой пример использования сигнальной переменной:
struct timeval now;
struct timespec timeout;
gettimeofday(&now, 0);
timeout.tv_sec = now.tv_sec + 2; // 2 sec
timeout.tv_nsec = now.tv_usec * 1000; // nsec
retval=0;
pthread_mutex_lock(&mutex);
while(!somethingHappens() && retval==0)
{
retval=pthread_cond_timedwait(&condition, &mutex, &timeout);
}
pthread_mutex_unlock(&mutex);
Смысл использования pthread_cond_timedwait состоит в том, что мы либо дожидаемся получения сигнала (pthread_cond_signal или pthread_cond_broadcast) в качестве уведомления о том, что somethingHappens(), или же прекращаем ожидание по истечении заданного нами таймаута. Во второй части фразы и кроется та самая потенциальная проблема! Обратите внимание, что время, передаваемое в качестве третьего параметра в pthread_cond_timedwait, задается в абсолютном виде! А что, если время будет переведено назад(!) после того, как мы получим текущее время (gettimeofday) и до того, как уснем в ожидании на pthread_cond_timedwait?
Каково будет поведение pthread_cond_timedwait, если наш процесс уже спит на этом вызове? Здесь все чисто! На всех платформах, на которых я проводил эксперимент с переводом времени назад, изменение просто игнорировалось, т.е. реально внутри вызова время все-таки преобразуется из абсолютного к относительному значению. Интересно, почему это не вынесено в интерфейс функции? Это бы решило все проблемы!
Критики могут возразить, что это какая-то пренебрежимо маловероятная ситуация, чтобы перевод системного времени попал именно в этот ничтожно малый кусок кода. Позвольте не согласиться. С одной стороны, если вероятность какого-либо события не равна нулю, то оно обязательно случится (принято называть это «генеральским эффектом»), а с другой стороны все сильно зависит от конкретной программы. Мы столкнулись с этой проблемой при разработке системы видеонаблюдения, а это десятки потоков (thread-ов), в каждом из которых делается pthread_cond_timedwait раз эдак 25 в секунду, а перевод времени на час назад приводил к тому, что с вероятностью, близкой к 100%, какой-нибудь поток да и заснет на этот час плюс 1/25 секунды!
Что делать?
Как я уже сказал в начале своего повествования, красивого решения данной проблемы нет, но и не решать ее вовсе, нельзя! В нашей системе мы организовали отдельный поток, назовем его «потоком мониторинга системного времени», который отслеживает «переводы времени назад» и в случае их выявления «будит» все сигнальные переменные. Т.е. по сути, решение предполагает наличие в системе некоторого выделенного менеджера, в котором необходимо зарегистрировать все используемые сигнальные переменные. Получилось что-то такое:
class SystemTimeManager
{
public:
SystemTimeManager();
~SystemTimeManager();
void registerCond(pthread_mutex_t *mutex, pthread_cond_t *cond);
void unregisterCond(pthread_cond_t *cond);
private:
static void *runnable(void *ptr);
private:
time_t _prevSystemTime;
pthread_t _thread;
bool _finish;
pthread_mutex_t _mutex;
std::map<pthread_cond_t *, pthread_mutex_t *> _container;
};
SystemTimeManager::SystemTimeManager ():
_prevSystemTime(time(0)),
_finish(false)
{
pthread_mutex_create(&_mutex, 0);
pthread_create(&_thread, 0, runnable, this);
}
SystemTimeManager::~SystemTimeManager()
{
_finish=true;
pthread_join(_thread, 0);
pthread_mutex_destroy(&_mutex);
}
void SystemTimeManager::registerCond(pthread_mutex_t *mutex, pthread_cond_t *cond)
{
pthread_mutex_lock(&_mutex);
_container.insert(std::make_pair(cond, mutex));
pthread_mutex_unlock(&_mutex);
}
void SystemTimeManager::unregisterCond(pthread_cond_t *cond)
{
pthread_mutex_lock(&_mutex);
std::map<pthread_cond_t *, pthread_mutex_t *> it=_container.find(cond);
if(it!=_container.end())
_container->erase(it);
pthread_mutex_unlock(&_mutex);
}
void * SystemTimeManager::runnable(void *ptr)
{
SystemTimeManager *me=reinterpret_cast< SystemTimeManager *>(ptr);
while(!_finish)
{
If(time(0)<_prevSystemTime)
{
pthread_mutex_lock(&me->_mutex);
for(std::map<pthread_cond_t *, pthread_mutex_t *> it=_container.begin();
it!=_container.end(); ++it)
{
pthread_mutex_lock(it->second);
pthread_cond_broadcast(it->first);
pthread_mutex_unlock(it->second);
}
pthread_mutex_unlock(&me->_mutex);
}
_prevSystemTime=time(0);
sleep(1);
}
}
Теперь нам достаточно создать экземпляр класса SystemTimeManager и не забыть зарегистрировать в нем все используемые нами сигнальные переменные.
В заключение хотелось бы обратить внимание уважаемого сообщества на тему данной статьи «проблема, решение, дискуссия». Проблему, я надеюсь, описал достаточно понятно. Решение описанной проблемы, пусть и не самое элегантное, я привел – надеюсь, оно кому-нибудь будет полезно. Однако последнее – дискуссию — я никак не могу сделать без вас, уважаемые читатели. Может быть, у кого-то есть какие-то другие, более элегантные решения данной проблемы?
Автор: isvirin