Реализация таймера на C++11

      Комментарии к записи Реализация таймера на C++11 отключены

Помечено: , , ,

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 1 неделя, 1 день назад.

  • Автор
    Сообщения
  • #4110

    Понадобилось мне реализовать на современном С++ таймер, да не простой… В статье разобраны несколько вариантов таймера (от простого к интересному).

    Простейший таймер на C++

    Итак, самый простой таймер должен подождать заданное время, а потом вызвать нужную нам функцию. При этом возможны два варианта: приостановиться может основной поток или задержка и вызов функции могут быть вынесены в отдельный поток. Реализация:

    // timer.hpp:
    #ifndef TIMERHPP
    #define TIMERHPP
    
    #include <chrono>
    #include <functional>
    
    class Timer {
    public:
      Timer();
      void add(std::chrono::milliseconds delay,
               std::function<void ()> callback,
               bool asynchronous = true);
    };
    
    #endif

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

    // timer.cpp
    #include "timer.hpp"
    
    #include <thread>
    
    Timer::Timer() {
    }
    
    void Timer::add(std::chrono::milliseconds delay,
                    std::function<void()> callback,
                    bool asynchronous) {
      if (asynchronous) {
        std::thread([=]() {
          std::this_thread::sleep_for(std::chrono::milliseconds(delay));
          callback();
        }).detach();
      }
      else {
        std::this_thread::sleep_for(std::chrono::milliseconds(delay));
        callback();
      }
    }

    Если требуется асинхронное выполнение — создается новый объект потока и для него вызывается метод detach(), за счет этого поток работает независимо от основного потока. Функция std::this_thread::sleep_for останавливает поток на заданное время (в нашем случае задается в миллисекундах), после задержки вызывается наша callback-функция.

    // main.cpp
    #include "timer.hpp"
    #include <iostream>
    
    void foo() {
      std::cout << "hello\n";
    }
    
    void bar() {
      std::cout << "world\n";
    }
    
    int main() {
      Timer timer;
    
      timer.add(std::chrono::milliseconds(1000), bar, true);
      timer.add(std::chrono::milliseconds(500), foo);
    
      timer.add(std::chrono::milliseconds(2000), []{}, false);
    }

    В функции main в таймер добавляются три функции. Первой добавляется bar, однако у этой функции задана большая задержка, чем у foo — поэтому выполнится она позже. Третья функция добавляется асинхронной чтобы затормозить основной поток до тех пор, пока не завершат выполнение два других потока.

    Результат работы программы:

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

    Более сложный таймер

    У меня возникла специфическая задача — таймер в один момент времени должен выполнять только одну функцию — предыдущая функция должна быть остановлена (не должна она быть вызвана когда завершится sleep_for). Казалось бы — задача не сильно отличается от предыдущей, однако, std::thread не позволяет остановить поток, тем более, отсоединенный (после вызова detach). Конечно, после выполнения sleep_for мы можем проверить не были ли добавлены другие функции, но это не эффективно. Мой код должен был быть размещен на сервере и если на него придет, скажем, 1000 запросов — то при таком подходе будет создана 1000 потоков, что уже может положить систему. Лучший вариант, что я смог придумать — «тормозить» единственный поток на небольшое время, а при добавлении функции лишь изменять callback и параметры времени. Задача очень специфическая, но имеет интересную реализацию (можно чему-нибудь научиться):

    // timer.hpp
    #ifndef TIMERHPP
    #define TIMERHPP
    
    #include <chrono>
    #include <functional>
    #include <mutex>
    
    class Timer {
    public:
      Timer();
      void add(std::chrono::milliseconds delay,
               std::function<void ()> callback);
    private:
      bool m_is_running;
      std::mutex m_changing_mutex;
      std::chrono::milliseconds m_delay;
      std::chrono::high_resolution_clock::time_point m_start;
      std::function<void()> m_callback;
    };
    
    #endif
    

    Теперь нам точно нужен класс:

    • m_is_running хранит состояние таймера — true если в таймер добавлена функция, которую надо выполнить (она еще не была выполнена);
    • m_delay хранит задержку выполнения текущей функции;
    • m_start хранит время, когда в таймер была добавлена последняя функция;
    • m_callback — последняя добавленная в таймер функция;
    • все описанные выше параметры являются общими для главного потока и потока, замеряющего время — обращения к ним должна быть защищены мьютексом m_changing_mutex.

    // timer.cpp
    #include "timer.hpp"
    
    #include <thread>
    
    Timer::Timer() : m_is_running(false) {
      std::thread([=]() {
        while (true) {
          std::this_thread::sleep_for(std::chrono::milliseconds(100));
          std::lock_guard<std::mutex> lock(m_changing_mutex);
          if (m_is_running &&
              std::chrono::high_resolution_clock::now() - m_start > m_delay) {
            m_is_running = false;
            m_callback();
          }
        }
      }).detach();
    }
    
    void Timer::add(std::chrono::milliseconds delay,
                    std::function<void()> callback) {
      std::lock_guard<std::mutex> lock(m_changing_mutex);
      m_callback = callback;
      m_is_running = true;
      m_start = std::chrono::high_resolution_clock::now();
      m_delay = delay;
    }
    

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

    Функция добавления функции в таймер захватывает ресурс (работает с тем же муьютексом) и затем изменяет все параметры.

    В этом коде я предлагаю обратить внимание на работу с мьютексом (для тех, кто раньше на работал с std::lock_guard) — это выглядит так здорово потому, что реализована идиома RAII (этот же принцип заложен в умные указатели типа unique_ptr). Так, функция add блокирует мьютекс создавая объект std::lock_guard, при этом объект создается на стеке, т.е. при выходе из функции (в том числе при возникновении исключения) ресурс освобождается. Аналогичным образом ресурс захватывается начиная с создания объекта std::lock_guard до конца итерации во вспомогательном потоке, замеряющем время.

    Про замер времени можно прочитать в статье «Замерить время работы функции на С++«.

Для ответа в этой теме необходимо авторизоваться.