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

    Для Linux в Си в настоящее время есть следующие функции:

unsigned int sleep(unsigned int __seconds); ,

объявленная в файле <unistd.h> и

int nanosleep(const struct timespec *req, struct timespec *rem); ,

объявленная в файле <time.h>

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

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

    Таймер должен соответствовать следующим требования: должен минимально потреблять вычислительные ресурсы, должен мгновенно включаться/выключаться, при этом сбрасывать своё состояние и обязательно быть достаточно точным.

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

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

    Для Linux в Си набор функций и типов для работы с потоками определен в файле <pthread.h>. При компиляции необходимо добавлять ключ -lpthread. Синхронизация потоков (при доступе к общему ресурсу) может осуществляться мютексами и семафорами. И для практического использования всё выглядит достаточно просто.

    Новый класс таймера я решил назвать stimer, соответственно заголовочный файл stimer.h получился следующего вида, и он будет общий для всех решений:

#ifndef _STIMER_H_
#define _STIMER_H_

#include <stdlib.h>
#include "sfuns.h" //В sfuns.h просто определён тип t_bool

//Структура определяющая новый класс
struct stimer;
typedef struct stimer t_stimer;

//Структура событий.
typedef struct stimer_events {
    void (*on_time)(t_stimer* timer);
    void (*on_error)(t_stimer* timer, int* exception);
} t_stimer_events;

//Конструктор/деструктор
t_stimer* stimer_create(void* parent, int* exception);
void stimer_destroy(t_stimer* timer);

//Интервал
void stimer_set_interval(t_stimer* timer, int value);

//Активация/деактивация таймера
void stimer_set_active(t_stimer* timer, t_bool enable);
t_bool stimer_get_active(t_stimer* timer);

//Слушатель события
void stimer_set_listener(t_stimer* timer, t_stimer_events* listener);

//Фамилия родителя
void stimer_set_parent(t_stimer* timer, void* parent);
void* stimer_get_parent(t_stimer* timer);

#endif

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

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <termios.h>

#include "sfuns.h"
#include "stimer.h"

//Объявлена пара экземпляров таймера
t_stimer* timer1; 
t_stimer* timer2;
//Слушатель событий от таймера
t_stimer_events listener;

//Функции обработки событий
void ontime_events(t_stimer* timer)
{
    time_t curent_time;
    time(&curent_time);

    if(timer == timer1){
        printf("Event timer 1 %s",ctime(&curent_time));
    }
    if(timer == timer2){
        printf("Event timer 2 %s",ctime(&curent_time));
    }
}

void onerror_events(t_stimer* timer, int* exception)
{
    if(timer == timer1){
        printf("Error timer 1 %d\n", *exception);
    }
    if(timer == timer2){
        printf("Error timer 2 %d\n", *exception);
    }
}

int main(int args, char** argv)
{
    unsigned char key;
    int err;

    //Присвоение слушателю событий его функций обработчиков
    listener.on_time = ontime_events;
    listener.on_error = onerror_events;

    //Инициализация объектов таймера. Т.к. родитель нам в
    //данном случае не нужен, то первым параметром пишем NULL 
    timer1 = stimer_create(NULL,&err);
    if(timer1 == NULL){
        printf("Ceate timer1. Error number: %d\n",err);
        return 0;
    }
    timer2 = stimer_create(NULL,&err);
    if(timer2 == NULL){
        printf("Ceate timer2. Error number: %d\n",err);
        stimer_destroy(timer1);
        return 0;
    }

    //Регистрация слушателя
    stimer_set_listener(timer1,&listener);
    stimer_set_listener(timer2,&listener);

    //Установка периодичности работы таймеров
    //Если использвать функцию sleep, то интервал будет задаваться в секундах.
    //Для функции nanosleep, в примерах, интервал будет задаваться в миллисекундах.
    stimer_set_interval(timer1, 3000);
    stimer_set_interval(timer2, 5000);

    stimer_set_active(timer1, true);
    stimer_set_active(timer2, true);

    //Обработка клавиатуры
    do {
        key = sgetch();
        printf("key: %d\n",key);
        if(key==49){ //1
            stimer_set_active(timer1, false);
            printf("timer1 active false\n");
        }
        if(key==50){ //2
            stimer_set_active(timer1, true);
            printf("timer1 active true\n");
        }
        if(key==51){ //3
            stimer_set_active(timer2, false);
            printf("timer2 active false\n");
        }
        if(key==52){ //4
            stimer_set_active(timer2, true);
            printf("timer2 active true\n");
        }
    } while (key!=27); //ESC

    stimer_destroy(timer1);
    stimer_destroy(timer2);

    return 0;
}

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

#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

#include "sfuns.h"
#include "stimer.h"

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

//Для одноразовой инициализации мьютекса и реализации патерна "одиночка"
//просто ввел локальную глобальную переменную 
static t_bool single_init = false;

//Подсчёт количества экземпляров таймера необходим для того, 
//что бы в деструкторе понимать, что у нас ещё кто-то остался или нет.
static int n_timers = 0;

//Структура класса с приватными полями
struct stimer {
    int interval;      //Периодичность работы
    t_bool enable;     //Состояние активности 
    pthread_t tid;     //Идентификатор потока
    t_stimer_events* listener; //Слушатель событий
    void* parent;              //Чьих рода будет
};

//Фукция отдельного потока
void* execute_thread(void* arg)
{
    //Передача указателя на экзем созданного объекта таймера
    t_stimer* timer = (t_stimer*) arg;
    
    //Включает возможность немедленного завершения потока
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

    do {
        //Отправляет поток в сон на заданный интервал (в секундах)
        sleep(timer->interval); 
        
        //Так как для обработки событий, для всех потоков, используется 
        //одна и таже функция, то доступ к ней синхронизируем мютексом
        pthread_mutex_lock(&mutex);
        timer->listener->on_time(timer);
        pthread_mutex_unlock(&mutex);

    //Так как поток может завершаться немедленно,
    //то каких-то условий для выхода из цикла не требуется         
    } while(1);  
}

//Функция конструктор
t_stimer* stimer_create(void* parent, int* exception)
{
    int status;

    //Выделяем память под новый объект
    t_stimer* new_stimer = malloc(sizeof(t_stimer));
    //Проверяем, что всё замечательно или возвращаемся с кодом ошибки
    if(new_stimer == NULL){
        *exception = errno;
        return NULL;
    } 

    //Инициализируем переменные структуры-таймера
    new_stimer->enable = false;
    new_stimer->interval = 1;
    new_stimer->parent = parent;
    
    n_timers++;
    
    //Однократная инициализация общего мютекса
    if(single_init == false) {
       status = pthread_mutex_init(&mutex,NULL);
       if(0!=status){
           free(new_stimer);
           *exception = status;
           return NULL;
       }
       single_init = true;     
    }
    
    return new_stimer;
}

//Функция деструктор
void stimer_destroy(t_stimer* timer)
{
    if(n_timers > 0){
        if(timer->enable == true){
            pthread_cancel(timer->tid);
            timer->enable = false;
        }
        free(timer);
        timer = NULL;
        n_timers--;
        if(n_timers == 0){
            pthread_mutex_destroy(&mutex);
            single_init = false;
        }
    }
}

//Включение и выключение таймера
void stimer_set_active(t_stimer* timer, t_bool enable)
{ 
    int status;

    if((timer->enable == false) && (enable == true)){
        status = pthread_create(&timer->tid,0,execute_thread,timer);
        if(0!=status){
            timer->listener->on_error(timer,&status);
        }
    }
    if((timer->enable == true) && (enable == false)){
        status = pthread_cancel(timer->tid);
        if(0!=status){
            timer->listener->on_error(timer,&status);
        }
    }
    timer->enable = enable;
}
//Во время включения и выключения таймера создаётся и соответственно
//уничтожает дополнительный поток. На это тратятся ресурсы, что не соответствует
//предъявляемым к таймеру требованиям. Такая реализация имеет место быть
//только при условии не частой или единовременной активации таймера.

//Далее функции "сеттеры" и "геттеры" для доступа к переменным структуры 
void stimer_set_interval(t_stimer* timer, int value)
{
    timer->interval =value;
}

t_bool stimer_get_active(t_stimer* timer)
{
    return timer->enable;
}

void stimer_set_listener(t_stimer* timer, t_stimer_events* listener)
{
    timer->listener = listener;
}

void stimer_set_parent(t_stimer* timer, void* parent)
{
    timer->parent = parent;
}

void* stimer_get_parent(t_stimer* timer)
{
    return timer->parent;
}

    Получилась откровенная "жесть". Считаю, что "крэшить" потоки и создавать их заново не очень хорошая идея. Решение простое, но на этом все плюсы и заканчиваются поэтому - "в топку".

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

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

    Данное решение опробовал с функцией nanosleep(). Исходя из названия, данная функция может обеспечивать задержки в наносекундах, а вот фактическая точность получается в миллисекундах, поэтому задание перемнной value для функции stimer_set_interval() выбрано в миллисекундах.

    Реализация имеет следующий вид:

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>

#include "sfuns.h"
#include "stimer.h"

static t_bool single_init = false;
static pthread_t tid = 0;
static t_bool terminate = false;

static int n_timers = 0;

struct stimer {
    int interval;

    struct timespec time_before;
    struct timespec time_after;

    t_bool enable;
    t_stimer_events* listener;
    void* parent;
};

static t_stimer** timers;

void* execute_thread(void* arg)
{
    int i;
    int interval;

    //Период контроля времени задаётся с точностью в 10мс.
    //Контролировать в данной реализации таймера точность в 1мс не имеет смысла,
    //так как это почти не возможно и, как правило, не требуется,
    //а крутить проверку таймеров с такой частотой только "пожерать" ресурсы процессора.

    struct timespec sleep_period = {0,9999999}; //Период, почти 10 мс

    do {
        for(i=0;i<n_timers;i++){
            if(timers[i]->enable == false){
                //Если таймер не активный, то присваиваем ему начальное значение
                clock_gettime(CLOCK_REALTIME, &timers[i]->time_before);
            }
        }
        //Засыпаем на 10мс
        nanosleep(&sleep_period , NULL);

        for(i=0;i<n_timers;i++){
            if(timers[i]->enable == true){
                //Получаем текущее значение времени.
                clock_gettime(CLOCK_REALTIME, &timers[i]->time_after);
                //Вычисляем прошедшее время ожидания
                interval = ((timers[i]->time_after.tv_sec-timers[i]->time_before.tv_sec)*1000000000 
                            +timers[i]->time_after.tv_nsec-timers[i]->time_before.tv_nsec)/1000000; 
                //Проверяем условие, если ОК, то обновляем время и формируем событие
                if(interval >= timers[i]->interval){
                    clock_gettime(CLOCK_REALTIME, &timers[i]->time_before);
                    timers[i]->listener->on_time(timers[i]);
                }
            }
        }
     } while (terminate == false);
}

t_stimer* stimer_create(void* parent, int* exception)
{
    int status;

    t_stimer* new_stimer = malloc(sizeof(t_stimer));
    if(new_stimer == NULL){
        *exception = errno;
        return NULL;
    }
    new_stimer->parent = parent;
    new_stimer->interval = 1000;
    new_stimer->enable = false;
    new_stimer->listener = NULL;
    clock_gettime(CLOCK_REALTIME,&new_stimer->time_before);

    n_timers++;
    timers = (t_stimer**)realloc(timers, n_timers*sizeof(t_stimer*));

    timers[n_timers-1] = new_stimer;

    if(single_init == false){
       terminate = false; 
       status = pthread_create(&tid,0,execute_thread,NULL);
       if(0!=status){
           *exception = status;
           n_timers=0;
           free(timers);
           free(new_stimer);
           new_stimer = NULL;
           return NULL;
       }
       single_init = true;
    }
    return new_stimer;
}

void stimer_destroy(t_stimer* timer)
{
    int i;
    int j;

    if(n_timers > 0){
        for(i=0;i<n_timers;i++){
            if(timer == timers[i]){
                for(j=i;j<(n_timers-1);j++){
                    timers[j]=timers[j+1];
                }
                timers = (t_stimer**)realloc(timers, (n_timers-1)*sizeof(t_stimer*));
                n_timers--;
                break;
            }
        }
        if(n_timers == 0){
            terminate = true;
            pthread_join(tid,NULL);
            single_init = false;
            timers = NULL;
        }
    }
    free(timer);
    timer = NULL;
}

void stimer_set_interval(t_stimer* timer, int value)
{
    timer->interval = value;
}

void stimer_set_active(t_stimer* timer, t_bool enable)
{
    timer->enable = enable;
}

t_bool stimer_get_active(t_stimer* timer)
{
    return timer->enable;
}

void stimer_set_listener(t_stimer* timer, t_stimer_events* listener)
{
    timer->listener = listener;
}

void stimer_set_parent(t_stimer* timer, void* parent)
{
    timer->parent = parent;
}
void* stimer_get_parent(t_stimer* timer)
{
    return timer->parent;
}

    Уже лучше, но в данной реализации самый большой минус это постоянная пустая прокрутка потока с большой частотой, а тратить на бездельников, пусть даже в пике, 0.5% от CPU (показания htop) это как-то дороговато. Так, что так делать тоже не годится.

    Ализируя полученный результат решил задачу таймера следующим образом:

  1. Для каждого экземпляра таймера создаётся свой индивидуальный поток.

  2. Для задержки потоков на заданный интервал используется функция nanosleep(). Точность интервала в миллисекунду более чем достаточна для решения, я не побоюсь предположить, 90% задач. 

  3. Что бы таймер в выключенном состоянии не тратил ресурсы и умел спать неограниченное время, для каждого потока ввёл так называиваемую "переменную условия".

  4. Немедленный сбос таймера и выход из функции nanosleep() может быть реализован через отправку сигнала SIGUSR1 или SIGUSR2 соответствующему потоку.

То,что получилось:

#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>

#include "sfuns.h"
#include "stimer.h"

//Мьютекс необходим для синхронизации доступа к общим ресурсам
static pthread_mutex_t mutex;

//Переменная необходима для однократной инициализации переменных в конструкторе
static t_bool single_init = false;

static int n_timers = 0;

struct stimer {
    //Свойства таймера
    struct timespec sleep_period;
    t_bool enable;

    //Переменные для работы с потоком
    pthread_t tid;
    t_bool terminate;

    //Те, с кем взаимодействует объект таймера
    t_stimer_events* listener;
    void* parent;

    /* Бездействие таймера в режиме "выключен"
    будет блокироваться через переменную условия */
    pthread_cond_t cond_active;
    pthread_mutex_t cond_lock;
};

void* execute_thread(void* arg)
{
    int status;

    t_stimer* timer = (t_stimer*) arg;

    do {
        //Блокировка потока через переменную условия
        pthread_mutex_lock(&timer->cond_lock);
            if(timer->enable == false){
                /* Если таймер выключен, то засыпаем и ждём "будильник",
                сигнал условия деблокировки. */
                pthread_cond_wait(&timer->cond_active,&timer->cond_lock);
            }
        pthread_mutex_unlock(&timer->cond_lock);

        /* Если nanosleep нормально доспала, то можно сформировать событие,
        а для этого необходимо контролировать статус */
        status = nanosleep(&timer->sleep_period, NULL);

        if(timer->enable == true && status == 0){
            pthread_mutex_lock(&mutex);
                timer->listener->on_time(timer);
            pthread_mutex_unlock(&mutex);
        }
    } while(timer->terminate == false);
}

void signal_handler(int sig)
{
    /*  Пустой обработчик сигнала.
    Фактически данная функция здесь нужна толь для
    того что бы переопределить реакцию потока на сигнал SIGUSR1.
    По умолчанию реакция на сигнал это "завершение работы".
    Сигнал необходим чтобы прервать сон nanosleep.*/
}

t_stimer* stimer_create(void* parent, int* exception)
{
    int status;

    t_stimer* new_stimer = malloc(sizeof(t_stimer));
    if(new_stimer == NULL){
        *exception = errno;
        return NULL;
    }

    new_stimer->enable = false;
    new_stimer->terminate = false;
    new_stimer->sleep_period.tv_sec=1;
    new_stimer->sleep_period.tv_nsec=0L;
    new_stimer->parent = parent;

    if(single_init == false) {
       //Переопределяем обработчик
       signal(SIGUSR1,signal_handler);

       //Инициализируем общий мьютекс
       status = pthread_mutex_init(&mutex,NULL);
       if(0!=status) goto crash_object;
       single_init = true;
    }
    status = pthread_mutex_init(&new_stimer->cond_lock,NULL);
    if(0!=status) goto crash_object;

    status = pthread_create(&new_stimer->tid,0,execute_thread,new_stimer);
    if(0!=status){
        pthread_mutex_destroy(&new_stimer->cond_lock);
        goto crash_object;
    }
    n_timers++;
    return new_stimer;

    crash_object: //Здесь сворачиваемся если что-то пошло не так.
    if((0 == n_timers) && (single_init == true)){
        pthread_mutex_destroy(&mutex);
        single_init = false;
    }
    free(new_stimer);
    *exception = status;
    return NULL;
}

void stimer_set_active(t_stimer* timer, t_bool enable)
{ 
    timer->enable = enable;
    //Будим таймер, если он спит
    pthread_mutex_lock(&timer->cond_lock);
    pthread_cond_signal(&timer->cond_active);
    pthread_mutex_unlock(&timer->cond_lock);

    //Прерываем и сбрасываем nanosleep
    pthread_kill(timer->tid,SIGUSR1);
}

void stimer_destroy(t_stimer* timer)
{
    if(n_timers > 0){
        //Останавливаем таймер
        timer->terminate = true;
        stimer_set_active(timer,false);
        //Ждем завершения потока
        pthread_join(timer->tid,NULL);
        //Далее всё чистим
        pthread_mutex_destroy(&timer->cond_lock);

        free(timer);
        timer = NULL;
        n_timers--;
        if(n_timers == 0){
            pthread_mutex_destroy(&mutex);
            single_init = false;
        }
    }
}
 
void stimer_set_interval(t_stimer* timer, int value)
{
    ldiv_t t = ldiv(value, 1000);
    timer->sleep_period.tv_sec = t.quot;
    timer->sleep_period.tv_nsec = t.rem * 1000000;
}

t_bool stimer_get_active(t_stimer* timer)
{
    return timer->enable;
}

void stimer_set_listener(t_stimer* timer, t_stimer_events* listener)
{
    timer->listener = listener;
}

void stimer_set_parent(t_stimer* timer, void* parent)
{
    timer->parent = parent;
}

void* stimer_get_parent(t_stimer* timer)
{
    return timer->parent;
}

   Может и не много пояснений, но из контекста кода и комментариев всё на первый взгляд должно быть понятно. С таймером надеюсь разобрались.

Файлы с исходным кодом можно скачать на https://github.com/fsdkm/C. Все дальнейшие "поделки" я буду выкладывать в данную папку.

PS:     

    Сеть Ethernet даёт Internet. Для Support Online Connect подавай. 

    Поэтому далее в соответствии с принятым ранее шаблоном ООП хочу «поюзать» Socket -ы. А начну с клиента TCP.

-

Комментарии (12)


  1. Alexwoodmaker
    24.07.2021 10:21
    +3

    На ASM не пробовали?


    1. Andn76 Автор
      24.07.2021 23:52

      Нет. С ASM-мом заморачивался только для STM8 когда дисплей через I2C запускал и то только что бы понять как же работает в определённых моментах компилятор Си.


  1. pfzim
    24.07.2021 11:03
    +3

    Выложите на guthub исходники, а не архив. Не удобно смотреть


  1. FD4A
    24.07.2021 11:05

    При использовании нанослипа не будет ошибка разве набегать?


  1. ncr
    24.07.2021 11:40
    +1

    //Функция деструктор
    void stimer_destroy(t_stimer* timer)
    {
        ...
        timer = NULL;
        ...
    }

    А в чем смысл обнуления локальных переменных?


  1. orignal
    24.07.2021 15:22

    А чем плох timerfd?


    1. Andn76 Автор
      24.07.2021 23:34

      А ничем. Тоже хороший вариант. Причём его можно в event poll отправить. Да, в epoll вообще можно все события отправить...


  1. sergio_nsk
    24.07.2021 19:59

    if(key==50){ //2

    Недавно была статья про комментирования в коде. Ваш случай - хороший пример того, как не надо делать. Вот как надо:

    if(key=='2'){

    Почему if ... if, а не if ... else if, два условия ведь никогда не выполняются. Почему не switch?

    Функция getch возвращает int, вы сохраняете результат в unsigned char. Здравствуй, предупреждение компилятора. Здравствуй, зависшая программа на Ctrl+d.


    1. Andn76 Автор
      24.07.2021 23:04
      -1

      switch не switch в main файле разницы нету. Про getch согласен - косяк


    1. Andn76 Автор
      25.07.2021 00:01
      -1

      Для себя я обычно в коде комментарии вообще не пишу.


  1. Andn76 Автор
    24.07.2021 23:27
    -1

    1. Я согласен, что есть errno, но я так привык и всегда так делал, если возникает исключение, то исключение возвращаться в виде события. Причём обычно оно не только включает цифровой идентификатор, но и текст. Потому что если пишешь, например компонент для работы с каким-то девайсом, ПЧ или ещё чего-нибудь, то когда в нем что-то происходит, удобней получать собственные комментарии через функцию и записывать их сразу в лог.

    2. Set и Get - наверное влияние Java. Но на скорость не влияет.


  1. ya_ne_znau
    25.07.2021 01:18

    Появились вопросы:

    1. зачем err, если есть стандартный errno?

    2. ооп на чистом C, это, конечно, хорошо, но только когда его соблюдать: почему "set_active / get_active"? почему не enable + disable + is_active?