It bothered me that my previous simple timer implementation fired off a new thread for each timeout and interval. I knew things could be done better, but didn’t yet know how. Well this morning inspiration came and I implemented new and shiny timer class. The interface is simple: you create a timer with 1 parameter, its “tick”. The tick determines how frequently the internal thread wakes up and looks for work. Work can be a repeating interval event, or a one time timeout event. Each time you register an interval or a timeout you get back a pointer to an event object. Using this event object you can cancel the interval or the timeout, if it hasn’t fired already. The internal thread lives for as long as the timer object does. It also self-corrects any time drift caused by the firing of events and execution delay. Complete implementation can be found at GitHub.
Here’s how you use it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> #include <sstream> #include "timer.h" using namespace std; using namespace chrono; int main() { auto start = high_resolution_clock::now(); auto duration = [start]() { auto now = high_resolution_clock::now(); auto msecs = duration_cast<milliseconds>(now - start).count(); stringstream ss; ss << msecs / 1000.0; cout << "elapsed " << ss.str() << "s\t: "; }; cout << "start" << endl; timer t(1ms); auto e1 = t.set_timeout(3s, [&]() { duration(); cout << "timeout 3s" << endl; }); auto e2 = t.set_interval(1s, [&]() { duration(); cout << "interval 1s" << endl; }); auto e3 = t.set_timeout(4s, [&]() { duration(); cout << "timeout 4s" << endl; }); auto e4 = t.set_interval(2s, [&]() { duration(); cout << "interval 2s" << endl; }); auto e5 = t.set_timeout(5s, [&]() { duration(); cout << "timeout that never happens" << endl; }); e5->signal(); // cancel this timeout this_thread::sleep_for(5s); e4->signal(); // cancel this interval cout << "cancel interval 2" << endl; this_thread::sleep_for(5s); cout << "end" << endl; } |
start
Program output.
elapsed 1s : interval 1s
elapsed 2s : interval 1s
elapsed 2s : interval 2s
elapsed 3s : timeout 3s
elapsed 3s : interval 1s
elapsed 4s : interval 1s
elapsed 4s : timeout 4s
elapsed 4s : interval 2s
elapsed 5s : interval 1s
cancel interval 2
elapsed 6s : interval 1s
elapsed 7s : interval 1s
elapsed 8s : interval 1s
elapsed 9s : interval 1s
elapsed 10s : interval 1s
end
The timer class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
#pragma once #include <thread> #include <chrono> #include <memory> #include <functional> #include <set> #include <iterator> #include <cassert> #include "event.h" class timer { public: template<typename T> timer(T&& tick) : m_tick(std::chrono::duration_cast<std::chrono::nanoseconds>(tick)), m_thread([this]() { assert(m_tick.count() > 0); auto start = std::chrono::high_resolution_clock::now(); std::chrono::nanoseconds drift{0}; while(!m_event.wait_for(m_tick - drift)) { ++m_ticks; auto it = std::begin(m_events); auto end = std::end(m_events); while(it != end) { auto& event = *it; ++event.elapsed; if(event.elapsed == event.ticks) { auto remove = event.proc(); if(remove) { m_events.erase(it++); continue; } else { event.elapsed = 0; } } ++it; } auto now = std::chrono::high_resolution_clock::now(); auto realDuration = std::chrono::duration_cast<std::chrono::nanoseconds>(now - start); auto fakeDuration = std::chrono::duration_cast<std::chrono::nanoseconds>(m_tick * m_ticks); drift = realDuration - fakeDuration; } }) {} ~timer() { m_event.signal(); m_thread.join(); } template<typename T, typename F, typename... Args> auto set_timeout(T&& timeout, F f, Args&&... args) { assert(std::chrono::duration_cast<std::chrono::nanoseconds>(timeout).count() >= m_tick.count()); auto event = std::make_shared<manual_event>(); auto proc = [=]() { if(event->wait_for(std::chrono::seconds(0))) return true; f(args...); return true; }; m_events.insert({ event_ctx::kNextSeqNum++, proc, static_cast<unsigned long long>(std::chrono::duration_cast<std::chrono::nanoseconds>(timeout).count() / m_tick.count()), 0, event }); return event; } template<typename T, typename F, typename... Args> auto set_interval(T&& interval, F f, Args&&... args) { assert(std::chrono::duration_cast<std::chrono::nanoseconds>(interval).count() >= m_tick.count()); auto event = std::make_shared<manual_event>(); auto proc = [=]() { if(event->wait_for(std::chrono::seconds(0))) return true; f(args...); return false; }; m_events.insert({ event_ctx::kNextSeqNum++, proc, static_cast<unsigned long long>(std::chrono::duration_cast<std::chrono::nanoseconds>(interval).count() / m_tick.count()), 0, event }); return event; } private: std::chrono::nanoseconds m_tick; unsigned long long m_ticks = 0; manual_event m_event; std::thread m_thread; struct event_ctx { bool operator < (const event_ctx& rhs) const { return seq_num < rhs.seq_num; } static inline unsigned long long kNextSeqNum = 0; unsigned long long seq_num; std::function<bool(void)> proc; unsigned long long ticks; mutable unsigned long long elapsed; std::shared_ptr<manual_event> event; }; using set = std::set<event_ctx>; set m_events; }; |