在项目中,经常会遇到这种场景:在特定的时间点去执行一些任务,这就是定时任务。

如何实现定时任务呢?如果不用任何技巧,我们可以把当前线程睡一睡,睡到特定的时间点再起来执行特定任务。看起来是解决了这个问题,但是,如果在睡一睡的这个过程,我还想执行一些其他的任务,怎么办?好像也可以解决,开一条新的线程去睡;如果在睡的过程中我不想再执行这个任务了,怎么办,直接中断线程?C++11没有这个机制,虽然这个机制我们可以自己实现,但是这显得很啰嗦;而且,还有可能会搞多个定时任务,如果不用一些特殊的技巧或设计,定时任务将会非常复杂及难以维护和可用。

究其上面的这些痛点,其根本原因是我们把定时任务的管理交给了我们自己。导致定时任务分散,不易于管理;既然分散,那么我们就集权。把所有定时任务交给一个人去管理。这个人的任务只有一个,管理我们的定时任务。这样的好处是,他一个人控制了所有,那么对定时任务的操作就有了统一的入口。

这个管理定时任务的人,我们就称之为定时任务器。

设计

我们把这个任务定时器该如何设计呢?首先,它肯定管理这很多定时任务,因此这需要一个数据结构来保存这些定时任务;其次,遵循我们之前让线程睡一睡的想法,任务定时器肯定还需要管理一条睡一睡的线程;当到达特定时间点时,就要执行任务,如果有很多人都想在同一时间点执行任务,那么我们就把这些任务放到线程池中来执行,因此,还需要一个线程池。

上面就是任务定时器需要的东西,有保存定时任务的数据结构,睡一睡线程,还有一个线程池。工作的逻辑在睡一睡线程中,那就来看看睡一睡线程的工作过程,这也是实现的基本蓝图。

这里面要解释的一件事是,这个里面说的定时器是异步定时器,因此,执行任务的那根线是虚线,这是因为执行任务在另一条线程中跑。

定时器的原理就是这些了。下面来看看定时器的两个种类:绝对定时器,相对定时器。

标准库中和时间挂钩的函数有this_thread::sleep_for / sleep_untilcondition_variable::wait_for / wait_until。这些函数都提供了两个版本,相对时间及绝对时间。相对时间用的是时间段,而绝对时间用的是时间点。

这两个有很大的区别,如果用绝对时间,就要提防着系统时间被别人篡改(这个在代码中也是做了处理的);用相对时间就没有这个问题,但是相对时间不能没有确切的时间点(虽然相对定时器也是通过时间点做的)。

相对定时器和绝对定时器用到是标准库中的两个时间类:

  • std::chrono::steady_clock
  • std::chrono::system_clock

steady_clock不能得到具体时间点,而system_clock可以通过to_time_t的到系统时间点。这也是为什么这两个clock分别适用于相对定时器和绝对定时器。

功能

绝对定时器

绝对定时器支持在将来的某一时间点执行特定的任务,如果系统时间遭到篡改,绝对定时器必须能发现并依然正确工作。

相对定时器

相对定时器,可以执行周期性任务,且支持精准周期(假设两秒执行一次任务,那么就一定是两秒执行一次任务,不管这个任务会执行多长时间)和模糊周期(相比于精准周期,任务必须一次接着一次执行,不允许周期任务并行执行)。

完整代码

代码比较长,后面会有关键地方的详细解析,而且,我写代码不喜欢在很明显的地方写注释,基本上从代码的名字及其结构就能读懂其含义。

先来看看类图:

  • Timer是定时器逻辑的基类
  • TimerTask是任务的基类
  • Timer使用了TimerTask
  • 然后就是自己去继承了

Timer

template<typename TimerTask>
class Timer
{public:typedef typename TimerTask::TimePoint TimePoint;typedef std::shared_ptr<TimerTask> ptrTimerTask;typedef typename std::multimap< std::string, ptrTimerTask >::iterator iterName2TaskMap;typedef typename std::multimap< TimePoint, ptrTimerTask>::iterator iterClock2TaskMap;public:void Shutdown(){std::lock_guard<std::mutex> lock(mutexVariable_);run_.store(false);if (thread_.joinable()){event_.NotifyOne();thread_.join();}}void AddTimerTask(ptrTimerTask task){{std::lock_guard<std::mutex> lock(mutexMap_);name2taskMap_.insert(std::make_pair(task->Name(), task));clock2taskMap_.insert(std::make_pair(task->End(), task));}event_.NotifyOne();Start();}void CancelTimerTask(const std::string & name){std::vector<ptrTimerTask> tasks;{std::lock_guard<std::mutex> lock(mutexMap_);std::pair<iterName2TaskMap, iterName2TaskMap> range = name2taskMap_.equal_range(name);std::copy(range.first, range.second, std::back_inserter(tasks));}std::for_each(tasks.begin(), tasks.end(), [](ptrTimerTask task){task->Stop();RemoveTask(task);});}protected:Timer() : run_(false), pool_(3, true, 10){}~Timer(){Shutdown();}void Start(){std::lock_guard<std::mutex> lock(mutexVariable_);if (!run_.load()){run_.store(true);event_.Reset();thread_ = std::thread(std::bind(&Timer<TimerTask>::Run, this));}}void RemoveTask(ptrTimerTask task){return;std::lock_guard<std::mutex> lock(mutexMap_);std::pair<iterName2TaskMap, iterName2TaskMap> name2taskRange = name2taskMap_.equal_range(task->Name());name2taskMap_.erase(std::find_if(name2taskRange.first, name2taskRange.second,[task](const std::pair<std::string, ptrTimerTask> & ele){return task == ele.second;}));std::pair<iterClock2TaskMap, iterClock2TaskMap> clock2taskRange = clock2taskMap_.equal_range(task->End());clock2taskMap_.erase(std::find_if(clock2taskRange.first, clock2taskRange.second,[task](const std::pair<TimePoint, ptrTimerTask> & ele){return task == ele.second;}));}void UpdateTaskClock(ptrTimerTask task){std::lock_guard<std::mutex> lock(mutexMap_);std::pair<iterClock2TaskMap, iterClock2TaskMap> clock2taskRange = clock2taskMap_.equal_range(task->End());clock2taskMap_.erase(std::find_if(clock2taskRange.first, clock2taskRange.second,[task](const std::pair<TimePoint, ptrTimerTask> & ele){return task == ele.second;}));clock2taskMap_.insert(std::make_pair(task->Next(), task));}ptrTimerTask EarlyTask(){std::lock_guard<std::mutex> lock(mutexMap_);return clock2taskMap_.empty() ? nullptr : std::begin(clock2taskMap_)->second;}void Run(){for (ptrTimerTask task; run_.load(); ){task = EarlyTask();if (!task)event_.WaitFor(std::chrono::minutes(1));TimePoint now = TimerTask::Now(), end = task->End();if (now >= end)Execute(task);else if (!Wait(now, end))Execute(task);elsenullptr;}}void Execute(ptrTimerTask task){if (task->IsRun()){if (task->Repeat()){if (task->Precise()){pool_.Submit(std::bind([task](){task->CallBack();}));UpdateTaskClock(task);}else{pool_.Submit(std::bind([task](){task->CallBack();})).wait();UpdateTaskClock(task);}}else{pool_.Submit(std::bind([task](){task->CallBack();}));RemoveTask(task);}}}virtual bool Wait(const TimePoint & now, const TimePoint & end) = 0;protected:std::mutex mutexVariable_;std::atomic<bool> run_;std::thread thread_;Event event_;std::mutex mutexMap_;std::multimap<std::string, ptrTimerTask> name2taskMap_;std::multimap<TimePoint, ptrTimerTask> clock2taskMap_;ThreadPool pool_;
};

TimerTask

template<typename TimerClock>
class TimerTask
{public:typedef TimerClock Clock;typedef typename Clock::time_point TimePoint;typedef std::function<void()> TimerTaskCallBack;public:TimerTask(const std::string & name, const TimerTaskCallBack & callback, bool repeat, bool precise) : name_(name),run_(true),repeat_(repeat),precise_(precise),callback_(callback){}const std::string & Name(){return name_;}void CallBack(){assert(callback_);callback_();}bool IsRun(){return run_;}bool Precise(){return precise_;}bool Repeat(){return repeat_;}void Stop(){run_ = false;}virtual const TimePoint & Next() = 0; virtual const TimePoint & End() = 0;
private:bool run_;bool repeat_;bool precise_;std::string name_;TimerTaskCallBack callback_;
};

RelativeTimerTask

class RelativeTimerTask : public TimerTask<std::chrono::steady_clock>
{public:typedef Clock::duration WaitType;typedef TimerTask<std::chrono::steady_clock> base;public:RelativeTimerTask(const std::string & name, const TimerTaskCallBack & callback, const WaitType & duration, bool repeat, bool precise) :duration_(duration),end_(duration + Clock::now()),base(name, callback, repeat, precise){}static const TimePoint Now(){return Clock::now();}const TimePoint & Next() override{if (Precise())end_ += duration_;else {end_ = Now() + duration_;}return end_;}const TimePoint & End() override{return end_;}
private:TimePoint end_;WaitType duration_;
};

AbsoluteTimerTask

class AbsoluteTimerTask : public TimerTask<std::chrono::system_clock>
{public:typedef Clock::time_point WaitType;typedef TimerTask<std::chrono::system_clock> base;public:AbsoluteTimerTask(const std::string & name, const TimerTaskCallBack & callback, const WaitType & time_point) :point_(time_point),base(name, callback, false, false){}static const TimePoint Now(){return Clock::now();}const TimePoint & Next() override{assert(false);return point_;}const TimePoint & End() override{return point_;}
private:TimePoint point_;
};

RelativeTimer

class RelativeTimer : public Timer<RelativeTimerTask>
{typedef Timer<RelativeTimerTask> base;
public:void AddTimerTask(const std::string & name, const RelativeTimerTask::TimerTaskCallBack & callback, const RelativeTimerTask::WaitType & duration, bool repeat = false, bool precise = false){base::AddTimerTask(std::make_shared<RelativeTimerTask>(name, callback, duration, repeat, precise));}
private:bool Wait(const TimePoint & now, const TimePoint & end) override{return event_.WaitFor(end - now);}
};

AbsoluteTimer

class AbsoluteTimer : public Timer<AbsoluteTimerTask>
{typedef Timer<AbsoluteTimerTask> base;
public:void AddTimerTask(const std::string & name, const AbsoluteTimerTask::TimerTaskCallBack & callback, const AbsoluteTimerTask::WaitType & time_point){base::AddTimerTask(std::make_shared<AbsoluteTimerTask>(name, callback, time_point));}
private:bool Wait(const TimePoint & now, const TimePoint & end) override{std::chrono::seconds offset = std::chrono::seconds(1);auto diff = end - now;if (diff < offset)return event_.WaitFor(diff);else {event_.WaitFor(offset);return true;}}
};

细节

TimerRunExcute方法的相互制约

Run方法是定时器的工作逻辑,本来我是想着Run方法专门用来计时的,但是由于加入了相对计时器的模糊周期,使得Run方法想单独只进行计时任务变得很困难,可以看下上一个版本的RunExcute方法的代码:

void Run()
{for (ptrTimerTask task; run_.load(); ){task = EarlyTask();if (!task)event_.WaitFor(std::chrono::minutes(1));TimePoint now = TimerTask::Now(), end = task->End();if (now >= end)nullptr;else if (!Wait(now, end))Execute(task);elsenullptr;}
}void Execute(ptrTimerTask task)
{if (task->IsRun()){if (task->Repeat()){if (task->Precise()){pool_.Submit(std::bind([task](){task->CallBack();}));UpdateTaskClock(task);}else{pool_.Submit(std::bind([task, this](){task->CallBack();this->UpdateTaskClock(task);}));}}else{pool_.Submit(std::bind([task](){task->CallBack();}));RemoveTask(task);}}
}

可以看到,Excute方法和任务的执行是完全异步的,这样,Run方法就和任务的执行没有任何关系,专门用来计时,计时器会非常精准,但是,这样计时线程Run的效率会很低,原因是任务的下一次执行时间只有在执行完本次才能更新,这就导致Run中的for循环在任务下一次执行时间更新前一直在空转;于是,我又把模糊周期的定时任务改成同步的了,这样效率是没有问题了,但是因为这个改动会影响其定时器的精准性,原因是计时线程和任务执行线程有了同步的关系。

相较于各种因素(编码的复杂性,逻辑的清晰度,等等),我选择了完整代码中的实现方法,我觉得这应该最平衡的了。

于是,计时任务的流程就变成了这样:

绝对定时器中防篡改系统时间的措施

可以看到,AbsoluteTimer中的Wait方法和RelativeTimer中的Wait方法很不一样。这么做的原因就是处理篡改系统时间;

假设现在是7点,你提交了一个7点10的任务,按照程序的逻辑,会等待10分钟,然后执行任务,但是,现在我把时间改成了7点9分58秒,难道还要等待10秒吗?肯定不应该,那我们应该怎么办,既然你可能篡改时间,那么我就1秒钟看一次系统时间,这样可以有效地处理这个问题。

Timer中线程池的规模

一开始,线程池我只配置了一条线程,导致我在测试精准周期的定时任务时一直不精准,比如,我要执行一个周期为2秒的任务,但是这个任务执行完需要5秒。按理说,精准周期的任务,每隔两秒就会执行依次,但是,情况却是5秒执行一次。原因就是线程池中的线程少了,每两秒向线程池提交一个任务,而线程池又只有一条线程,于是任务只能一个一个执行,因此任务执行的间隔是5秒。

因此,我们应该扩大线程规模,那么,线程规模该多大呢?上面这种情况开三条线程就能满足精准周期定时任务的要求了,可以自己画下图,因此,线程规模=ceil(任务执行时间÷周期)线程规模 = ceil(任务执行时间 \div 周期)线程规模=ceil(任务执行时间÷周期)。

其他

其实还有很多细节,这里也说不全,自己动手实现一个定时器,写的过程中你就会发现有很多地方需要考虑,包括我写的这个定时器,我觉得很多地方还没有考虑得很周全,还有很多地方可以更加优化。

测试

这份代码我分别在Window的Visual Studio 2015和Linux的Ubuntu和Centos上都跑过,都没有问题,可以放心使用。

这里提供一下我的测试方法,我只测试了相对定时器,没有测试取消定时任务的接口,有兴趣的可以自己写代码测试。

RelativeTimer RTimer;int main()
{int i = 0;RTimer.AddTimerTask("TEST", [&i](){std::chrono::system_clock::time_point now = std::chrono::system_clock::now();std::time_t now_c = std::chrono::system_clock::to_time_t(now);std::cout << std::put_time(std::localtime(&now_c), "%F %T") << '\n';std::cout << "FlushHip" << " + " << i++ << std::endl;/* // */std::this_thread::sleep_for(std::chrono::seconds(5));}, std::chrono::seconds(2), true, false /* true*/);/*RTimer.AddTimerTask("TEST-TEST", [&i](){std::chrono::system_clock::time_point now = std::chrono::system_clock::now();std::time_t now_c = std::chrono::system_clock::to_time_t(now);std::cout << std::put_time(std::localtime(&now_c), "%F %T") << '\n';std::cout << "flushhip" << " + " << i++ << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5));}, std::chrono::seconds(2), true, false);*/while (true){std::this_thread::sleep_for(std::chrono::hours(1));}return 0;
}

/**/的地方可以轮换一下,看看控制台的输出是否准确。


参考:

  • 白话跨平台C++线程池实现
  • C++中的事件Event(基于条件变量的封装)
  • Zeus框架

聊聊C++任务定时器的设计与具体实现相关推荐

  1. android定时器课程设计,定时器课程设计.doc

    定时器课程设计定时器课程设计 摘要: 本设计通过使用89C52RC芯片核心,通过P3.4-P3.7口控制按键录入时间,P0.0-P0.7控制LED数码显示器时间,P2.3控制蜂鸣器定时器到达指定时间报 ...

  2. 聊聊那些专为算法设计的模式——模板方法模式

    AI越来越火热,人工智能已然成风!而人工智能最重要是各种算法,因此机器学习越来越受到追捧,算法越来越被重视. 作为一个算法的研究者,写出一手高级算法当然是令人兴奋的一件事!但你是否有时会有这种感觉: ...

  3. 高效软件定时器的设计

    摘要 软件定时器在协议栈等很多场景都有广泛的应用,有时候会有大量的定时器同时处于工作状态,它们的超时时间各异,要高效的保证每个定时器都能够较为准确的超时并执行到其回调函数并不是一件易事.本文分析嵌入式 ...

  4. 聊聊数据仓库中维度表设计的二三事

    前言 大家好,我是云祁!今天和大家聊聊数据仓库中维度表设计的那些事. 维度表是维度建模的灵魂所在,在维度表设计中碰到的问题(比如维度变化.维度层次.维度一致性.维度整合和拆分等)都会直接关系到维度建模 ...

  5. 聊聊那些专为算法设计的模式——访问模式

    AI越来越火热,人工智能已然成风!而人工智能最重要是各种算法,因此机器学习越来越受到追捧,算法越来越被重视. 作为一个算法的研究者,写出一手高级算法当然是令人兴奋的一件事!但你是否有时会有这种感觉: ...

  6. 面向对象设计原则_聊聊面向对象的6大设计原则

    程序员都知道编程有 3 大类:面向过程.面向对象.面向函数.面向对象是被讨论的最多的,个人认为,这是因为 Java 之类的编程语言有强大的用户基础,本质还是因为比较符合人的直觉. 说到面向对象,大家可 ...

  7. 架构 | 聊聊我心中的架构设计观

    [架构设计]| 总结/Edison Zhou 在各种面试场合,可能都会被问到"你对架构设计的理解",我也在最近的转正答辩中被技术委员会负责人问到,这里我重新整理一下思绪,聊聊我心中 ...

  8. 聊聊软件登录界面的设计与交互

    前面说了一堆废话,想看代码的可直接看第二章. 版本记录 日期 备注 2020-06-13 初稿 零.前言 这个登录界面提取自最近正在做的一个项目,此项目曾被我自豪地称为是公司数采软件的颜值担当,虽然这 ...

  9. PMCAFF产品众测 | 对话随手攒CEO聊聊这款产品的设计、推广和改进(活动已结束)

    PMCAFF产品经理社区全新栏目 PMCAFF产品众测 即日起正式开启!在这里你可以尽情发挥思路,参与每一款产品的设计.运营和改进,快来释放你体内的洪荒之力,这里就是你的主场! 获奖名单公布: 随手攒 ...

最新文章

  1. 从零开始搭建物联网平台(6):消息的持久化
  2. 简单的XML和JSON数据的处理
  3. websocket在web项目中的使用
  4. OpenCV中像素逻辑运算:逻辑非运算
  5. SaS中ne在mysql语句对应_SAS学习经验总结分享:篇四—SQL过程
  6. TCP/IP,HTTP,Socket的区别与联系
  7. Javascript面向对象编程(一):对象的产生
  8. 14岁AI天才的钢铁之心
  9. Element el-cascader 级联选择器详解
  10. 下载速度MB/s与Mb/s的区别
  11. 蚂蚁区块链BaaS平台应用开发指南(五):JS SDK的接入
  12. 河南省旅游服务中心信息中心备份及集成
  13. 如何部署搭建app服务端运行环境(java)?
  14. NumbericUtil工具类(实现数字及数字格式化的基本功能:精确的加减乘除法、金额数字转 成中文等。)
  15. 珠服务器维修,梦幻西游:最奇葩的服务器,避水珠比定魂珠贵10倍还供不应求...
  16. Android 7 Nougat 源码目录结构
  17. 累乘计算问题(C语言程序设计)
  18. vb treeview 展开子节点_电路的一般分析法(01)—节点电压法及其算例
  19. 小程序 动态实现进度条
  20. 为什么MySQL做查询语句时,第一次会很慢,但是第二次,第三次就会变快

热门文章

  1. 【玩转华为云】手把手教你利用ModelArts识别偶像的声音
  2. 计算机硬盘越大运行速度越大吗,电脑的内存越大越好吗?如果只加大内存,电脑反而会被拖慢!...
  3. 免费企业邮箱注册与收费的企业邮箱区别在哪
  4. 前端项目的创建和准备
  5. 为Linux服务器部署高效防毒软件
  6. Excel——使用OFFSET、MATCH、COUNTA实现二级菜单
  7. 从0开始一步一步用Laravel5.2集成原生微信支付
  8. CF1611E1 Escape The Maze (easy version)+ CF1611E2 Escape The Maze (hard version)
  9. 泰拳的快感之二——我看《冬荫功》
  10. f81沒有啟用配銷模組,全用INV的雜項處理方式處理,有以下管理要求