How to implement a correct time circulator detail in C++
- 2020-11-25 07:25:16
- OfStack
preface
In practical engineering, there may be a general requirement that a single thread in a service periodically completes a specific task at a fixed time interval. We abstract this problem as a time circulator.
Naive Way
class TimerCircle {
private:
std::atomic_bool running_{false};
uint64_t sleep_{0UL};
std::thread thread_;
public:
explicit TimerCircle(uint64_t s) : sleep_{s} {}
~TimerCircle() {
if (thread_.joinable()) {
terminate();
thread_.join();
}
}
TimerCircle(const TimerCircle&) = delete;
TimerCircle& operator=(const TimerCircle&) = delete;
TimerCircle(TimerCircle&&) = default;
TimerCircle& operator=(TimerCircle&&) = default;
public:
void launch() {
thread_ = std::move(std::thread(&TimerCircle::loop, this));
}
void terminate() {
running_.store(false);
}
void loop() {
running_.store(true);
while (running_.load()) {
do_something();
std::this_thread::sleep_for(std::chrono::seconds(sleep_));
}
}
private:
void do_something() const = 0;
};
Implementation simple trivial, 1 eye can see that there is no problem, so there is nothing to say.
The devil in the details
The only devil is in the details. If an object of type TimerCircle is destructed, the thread that destructs that object will be blocked for at most sleep_ seconds. If the cycle is long, say up to six hours, then this is clearly not acceptable.
To do this, we need the help of the wait_for function of the standard library's conditional variable std::condition_variable. First look at the function signature
template <typename Rep, typename Period, typename Predicate>
bool wait_for(std::unique_lock<std::mutex>& lock,
const std::chrono::duration<Rep, Period>& rel_time,
Predicate pred);
The function takes three arguments. lock is 1 unique_lock, which must be locked by the thread calling wait_for; rel_time is 1 time period, which means timeout time; pred is a predicate that returns either true or false.
Once called, the function blocks the current thread until two situations return:
Timeout. The function returns pred(). The condition variable is notified, and the predicate returns true; The function returns true.We can then implement an Countdown class
#include <chrono>
#include <condition_variable>
#include <mutex>
class Countdown final {
private:
bool running_ = true;
mutable std::mutex mutex_;
mutable std::condition_variable cv_;
public:
Countdown() = default;
~Countdown() = default;
Countdown(const Countdown&) = delete;
Countdown& operator=(const Countdown&) = delete;
Countdown(Countdown&&) = delete;
Countdown& operator=(Countdown&&) = delete;
public:
void terminate() {
{
std::lock_guard<std::mutex> lock(mutex_);
running_ = false;
}
cv_.notify_all();
}
template <typename Rep, typename Peroid>
bool wait_for(std::chrono::duration<Rep, Peroid>&& duration) const {
std::unique_lock<std::mutex> lock(mutex_);
bool terminated = cv_.wait_for(lock, duration, [&]() { return !running_; });
return !terminated;
}
};
So TimerCircle becomes
class TimerCircle {
private:
uint64_t sleep_{0UL};
Countdown cv_;
std::thread thread_;
public:
explicit TimerCircle(uint64_t s) : sleep_{s} {}
~TimerCircle() {
if (thread_.joinable()) {
terminate();
thread_.join();
}
}
TimerCircle(const TimerCircle&) = delete;
TimerCircle& operator=(const TimerCircle&) = delete;
TimerCircle(TimerCircle&&) = default;
TimerCircle& operator=(TimerCircle&&) = default;
public:
void launch() {
thread_ = std::move(std::thread(&TimerCircle::loop, this));
}
void terminate() {
cv_.terminate();
}
void loop() {
while (cv_.wait_for(std::chrono::seconds(sleep_))) {
do_something();
}
}
private:
void do_something() const = 0;
};
Simple and clear.
conclusion