Pthread_cond_timedwait: проблема, решение, дискуссия

в 21:55, , рубрики: condition, linux, multithreading, Mutex, Блог компании Нордавинд, метки: , , ,

Здравствуйте, уважаемые читатели!

Продолжая серию постов по многопоточному программированию, хочется коснуться одной фундаментальной проблемы использования сигнальных переменных в 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js