如何在C++中实现一个正确的时间循环器详解

时间:2022-05-10 05:37:24

前言

实际工程中可能会有这样一类普遍需求:在服务中,单独起一个线程,以一个固定的时间间隔,周期性地完成特定的任务。我们把这种问题抽象成一个时间循环器。

Naive Way

?
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
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;
};

实现简单平凡,一眼就能看出来没啥问题,于是也没啥好说的。

细节里的魔鬼

唯一的魔鬼藏在细节里。如果 TimerCircle 类型的对象发生析构,那么析构该对象的线程最多会被阻塞 sleep_ 秒。如果周期很长,比如长达 6 小时,那这显然是不可接受。

为此,我们需要借助标准库的条件变量 std::condition_variable 的 wait_for 函数的帮助。首先看其函数签名

?
1
2
3
4
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);

函数接受三个参数。lock 是一个 unique_lock,它必须为调用 wait_for 的线程所锁住;rel_time 是一个时间段,表示超时时间;pred 是一个谓词,它要么返回 true 要么返回 false。

一旦调用,函数会阻塞当前线程,直到两种情况返回:

  • 超时;此时函数返回 pred()。
  • 条件变量被通知,且谓词返回 true;此时函数返回 true。

于是我们可以实现一个 Countdown 类

?
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
#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;
 }
};

于是,TimerCircle 就变成

?
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
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;
};

简单,明了。

总结

到此这篇关于如何在C++中实现一个正确的时间循环器的文章就介绍到这了,更多相关C++实现时间循环器内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://liam.page/2020/10/14/implement-a-timed-looper-in-cxx/