Как сделать pthread_cond_timedwait() устойчивым к манипуляциям с системными часами?

Рассмотрим следующий исходный код, который полностью совместим с POSIX:

#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>

int main (int argc, char ** argv) {
    pthread_cond_t c;
    pthread_mutex_t m;
    char printTime[UCHAR_MAX];

    pthread_mutex_init(&m, NULL);
    pthread_cond_init(&c, NULL);

    for (;;) {
        struct tm * tm;
        struct timeval tv;
        struct timespec ts;

        gettimeofday(&tv, NULL);

        printf("sleep (%ld)\n", (long)tv.tv_sec);
        sleep(3);

        tm = gmtime(&tv.tv_sec);
        strftime(printTime, UCHAR_MAX, "%Y-%m-%d %H:%M:%S", tm);
        printf("%s (%ld)\n", printTime, (long)tv.tv_sec);

        ts.tv_sec = tv.tv_sec + 5;
        ts.tv_nsec = tv.tv_usec * 1000;

        pthread_mutex_lock(&m);
        pthread_cond_timedwait(&c, &m, &ts);
        pthread_mutex_unlock(&m);
    }
    return 0;
}

Печатает текущую системную дату каждые 5 секунд, однако она выполняет спящий режим в течение 3 секунд между получением текущего системного времени (gettimeofday) и ожиданием условия (pthread_cond_timedwait).

Сразу после печати "sleep (...)" попробуйте установить системные часы на два дня в прошлое. Что происходит? Ну, вместо того, чтобы ждать еще 2 секунды при условии, что это обычно происходит, pthread_cond_timedwait теперь ждет двух дней и 2 секунды.

Как это исправить?
Как я могу написать POSIX-совместимый код, который не прерывается, когда пользователь манипулирует системными часами?

Пожалуйста, имейте в виду, что системные часы могут меняться даже без взаимодействия с пользователем (например, клиент NTP может обновлять часы автоматически один раз в день). Установка часов в будущее не проблема, это приведет к тому, что сна рано проснется, что обычно не представляет проблемы, и которое вы можете легко "обнаружить" и обработать соответствующим образом, но установив часы в прошлое (например, потому что это было работающий в будущем, NTP обнаружил, что и исправил его) может вызвать большую проблему.

PS:
В моей системе не существует pthread_condattr_setclock() и CLOCK_MONOTONIC. Они обязательны для спецификации POSIX 2008 (часть "Base" ), но большинство систем по-прежнему соответствуют только спецификации POSIX 2004 на сегодняшний день и в спецификации POSIX 2004, эти два являются необязательными (расширенное расширение в реальном времени).

Ответы

Ответ 1

Интересно, что раньше я не сталкивался с этим поведением, но, опять же, у меня нет привычки часто сбрасывать с моим системным временем: -)

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

Другими словами, что-то вроде:

while (1) {
    sleep (10);
    pthread_cond_signal (&condVar);
}

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

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


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

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

Это очень похоже на тупик, о котором я упоминал выше, но скорее как неотъемлемая часть вашей архитектуры, чем болт.

Ответ 2

Вы можете защитить свой код от этой проблемы. Один простой способ - иметь один поток, единственной целью которого является просмотр системных часов. Вы сохраняете глобальный связанный список переменных условия, и если поток наблюдателя часов видит скачок системных часов, он передает каждую переменную условия в списке. Затем вы просто переносите pthread_cond_init и pthread_cond_destroy кодом, который добавляет/удаляет переменную условия в/из глобального связанного списка. Защитите связанный список с помощью мьютекса.