需求:后台常驻进程中实现定时调度(计划任务,每隔若干分钟执行任务)。
可模拟linux下cron的功能。
定义配置文件格式如下:
#[id] [start_time: yyyy-MM-dd-hh:mm] [period: min] [program] [params...] #1 2014-12-01-00:00 24 notepad a.txt task1 2015-09-10-12:00 1440 python %ABER_HOME%work.py -t 1 -s 2 taskB 2014-01-01-00:05 60 calc
其中"#"表示注释。
格式说明:
- id. 任务ID,保证各ID不重复即可。
- start_time. 任务第一次开始时间,格式为yyyy-MM-dd-hh:mm。
- period. 任务重复执行间隔,单位min。
- program. 程序名称。
- params. 程序参数,数量不定。
任务结构体设计:
typedef struct _TIME_ENTRY { QString taskId; //任务ID int period; //任务执行周期 QDateTime start_time; //第一次启动时间 QDateTime next_time; //预计下一次启动任务的时间 QString program; //程序名称 QStringList params; //参数列表 } TIME_ENTRY;
对任务调度器包装线程类:
class CTimerManager; class CTimerThread : public QThread { public: CTimerThread(CTimerManager &timerManager); void run(); private: bool m_run_tag; CTimerManager *mp_timer_manager; };
任务调度器:
class CTimerManager { friend class CTimerThread; public: CTimerManager(); ~CTimerManager(); void Init(); void Run(); public: static int CompareTimeEntry(const TIME_ENTRY &entry1, const TIME_ENTRY &entry2); //比较TimeEntry static QDateTime CalcNextTime(QDateTime start_time, int period, bool allowEqual); //计算下次运行时间 private: int ReloadTimeConfig(); //重新初始化:1.重载配置文件;更新任务Map int ReadTimeConfig(QMap<QString, TIME_ENTRY> &taskMap); //解析配置文件,存在临时Map里 void UpdateTimerMap(QMap<QString, TIME_ENTRY> &taskMap); //将临时Map合并到成员的Map里。 void CheckTaskMap(); //检查Map各任务是否到时间 int StartProgram(TIME_ENTRY &entry); //启动任务 private: QMap<QString, TIME_ENTRY> m_taskMap; QMap<QString, TIME_ENTRY> m_newTaskMap; CTimerThread *m_thd; //与线程对象紧密协作 };
线程启动代码:
CTimerThread::CTimerThread(CTimerManager &timerManager) { this->m_run_tag = false; this->mp_timer_manager = &timerManager; } void CTimerThread::run() { int cnt = 0; while(true) { if(--cnt <= 0) { cnt = RELOAD_CONFIG_CNT; this->mp_timer_manager->ReloadTimeConfig(); } this->mp_timer_manager->CheckTaskMap(); asleep(CHECK_INTERVAL * 1000); } }
调度器核心代码:
CTimerManager::CTimerManager() : m_thd(NULL) { } CTimerManager::~CTimerManager() { if(m_thd != NULL) { delete m_thd; } } int CTimerManager::ReloadTimeConfig() { qDebug() << "RELOAD"; this->ReadTimeConfig(m_newTaskMap); this->UpdateTimerMap(m_newTaskMap); return 0; } /** * 比较两个TIME_ENTRY * 忽略比较NextDateTime; * return 0:相同;1:不相同 **/ int CTimerManager::CompareTimeEntry(const TIME_ENTRY &entry1, const TIME_ENTRY &entry2) { if(entry1.period != entry2.period) { return 1; } if(entry1.start_time != entry2.start_time) { return 2; } if(entry1.program != entry2.program) { return 3; } if(entry1.params.size() != entry2.params.size()) { return 4; } else { for(int i=0; i<entry1.params.size(); i++) { if(entry1.params.at(i).compare(entry2.params.at(i)) != 0) { return 5; } } } return 0; } void CTimerManager::Init() { //ReloadTimeConfig(); } /** * 计算下次启动时间 * allowEqual: 是否允许下次时间与当前时间相同。 * 读配置文件时:允许;执行任务后,计算下一次执行时间:不允许 **/ QDateTime CTimerManager::CalcNextTime(QDateTime start_time, int period, bool allowEqual) { QDateTime cur_time = QDateTime::currentDateTime(); cur_time.setTime(QTime(cur_time.time().hour(), cur_time.time().minute(), 0)); QDateTime next_time; next_time = start_time; if(next_time > cur_time) { //下次时间晚于当前时间 return next_time; } while(next_time < cur_time) { int secsTo = next_time.secsTo(cur_time); int cnt = secsTo / PERIOD_UNIT / period; int mod = secsTo % (period * PERIOD_UNIT); if(mod != 0) { cnt += 1; } next_time = next_time.addSecs(period * PERIOD_UNIT * cnt); } if(next_time == cur_time) { if(!allowEqual) { next_time = next_time.addSecs(period * PERIOD_UNIT); } } return next_time; } /** * 读取定时任务配置文件 * 格式如下: * #[name] [start from: hour:min] [every: min] [program] [parameters] * #max period: 2147483min = 1491.308day * e.g. * n++ 12:40 60 notepad++ d:envisionlogupgrade_svr.log * calculator 00:00 1 calc * **/ int CTimerManager::ReadTimeConfig(QMap<QString, TIME_ENTRY> &taskMap) { taskMap.clear(); //Fix bug #23834 2014-12-19 QString config_filename = g_prjhome.c_str(); config_filename += TIME_CONFIG_FILENAME; QFile qfile(config_filename); if(qfile.exists()==false) { config_filename = g_prjhome.c_str(); config_filename += TIME_CONFIG_FILENAME_BAK; qfile.setFileName(config_filename); if(qfile.exists()==false) { return -1; } } if (qfile.open(QIODevice::ReadOnly | QIODevice::Text) == false) { return -1; } QTextStream in(&qfile); while (!in.atEnd()) { QString line = in.readLine(); line = line.simplified(); line = line.trimmed(); if (line.isEmpty() || line.startsWith("#")) { continue; } QStringList list = line.split(" ", QString::SkipEmptyParts); if(list.size() < 4) { continue; } TIME_ENTRY entry; entry.name = list.at(0); QDateTime aTime = QDateTime::fromString(list.at(1), "yyyy-MM-dd-hh:mm"); if(!aTime.isValid() || aTime < QDateTime::fromString("2000-01-01", "yyyy-MM-dd")) { qDebug() << QString("Time. not valid") << list.at(1); continue; } entry.start_time.setDate(aTime.date()); entry.start_time.setTime(QTime(aTime.time().hour(), aTime.time().minute(), 0)); bool isOk = false; int period = list.at(2).toInt(&isOk); if(!isOk) { qDebug() << QString("int not valid. ") << list.at(2); continue; } if(period > MAX_PERIOD_MINUTE) { period = MAX_PERIOD_MINUTE; } else if(period <=0) { period = 1; } entry.period = period; entry.next_time = CalcNextTime(entry.start_time, entry.period, true); entry.program = list.at(3); for(int i=0; i<4; i++) { list.removeFirst(); } entry.params = list; taskMap[entry.name] = entry; qDebug() << entry.name << entry.program << entry.next_time; } qfile.close(); return 0; } /** * 开线程 **/ void CTimerManager::Run() { if(this->m_thd != NULL) { return; } this->m_thd = new CTimerThread(*this); m_thd->start(); } void CTimerManager::CheckTaskMap() { QString name; QDateTime cur_time; cur_time = QDateTime::currentDateTime(); cur_time.setTime(QTime(cur_time.time().hour(), cur_time.time().minute(), 0)); foreach(name, this->m_taskMap.keys()) { if(this->m_taskMap.contains(name) == NULL) { qDebug() << "ERROR: cannot find " << name; } TIME_ENTRY &entry = this->m_taskMap[name]; if(cur_time >= entry.next_time) { entry.next_time = CalcNextTime(entry.next_time, entry.period, false); StartProgram(entry); } } } int CTimerManager::StartProgram(TIME_ENTRY &entry) { QDateTime cur_time = QDateTime::currentDateTime(); bool ret = QProcess::startDetached(entry.program, entry.params); if(ret == false) { qDebug() << "Cannot start. " << entry.name; return -1; } qDebug() << "Start. " << entry.name << entry.program << entry.next_time; return 0; } void CTimerManager::UpdateTimerMap(QMap<QString, TIME_ENTRY> &taskMap) { QString name; //在列表移除中新列表中不存在的任务 QMapIterator<QString, TIME_ENTRY> iterOld(m_taskMap); while(iterOld.hasNext()) { iterOld.next(); name = iterOld.key(); if(!taskMap.contains(name)) { this->m_taskMap.remove(name); qDebug()<<"remove "<<name << iterOld.value().program; } } //比对新列表与老列表,新增任务或修改任务 QMapIterator<QString, TIME_ENTRY> iterNew(taskMap); while(iterNew.hasNext()) { iterNew.next(); name = iterNew.key(); if(!this->m_taskMap.contains(name)) { //New task this->m_taskMap[name] = iterNew.value(); qDebug()<<"add "<<name << iterNew.value().program<< iterNew.value().next_time; } else { if(CompareTimeEntry(iterNew.value(), this->m_taskMap[name]) != 0) { this->m_taskMap[name] = iterNew.value(); qDebug()<<"modify "<<name << iterNew.value().program << iterNew.value().next_time; } } } }
CTimerManager与CTimeThread使用了交叉引用,CTimeManager::run()启动CTimerThread,CTimerThread启动while死循环,引用CTimerManager对象的reload(), check()等操作。
最终只需把CTimerManager暴露给外部,隐藏了CTimerThread的细节,解耦:
CTimerManager ctm;
ctm.Init();
ctm.Run();
CalcNextTime()函数是以start_time为起点,累加period,直到时间不早于当前时刻,每次必须和实时系统时间比较,保证正确性(不能简单存时刻,每次触发后增加period,这样万一有一次没触发,以后都将不触发;比较实时系统时间比较保险,有利于程序今后的扩展)。
函数有allowEqual参数,该参数为true适用于重载文件,为false适用于轮询过程。
/** * 计算下次启动时间 * allowEqual: 是否允许下次时间与当前时间相同。 * 读配置文件时:允许;执行任务后,计算下一次执行时间:不允许 **/ QDateTime CTimerManager::CalcNextTime(QDateTime start_time, int period, bool allowEqual) { QDateTime cur_time = QDateTime::currentDateTime(); cur_time.setTime(QTime(cur_time.time().hour(), cur_time.time().minute(), 0)); QDateTime next_time; next_time = start_time; if(next_time > cur_time) { //下次时间晚于当前时间 return next_time; } while(next_time < cur_time) { int secsTo = next_time.secsTo(cur_time); int cnt = secsTo / PERIOD_UNIT / period; int mod = secsTo % (period * PERIOD_UNIT); if(mod != 0) { cnt += 1; } next_time = next_time.addSecs(period * PERIOD_UNIT * cnt); } if(next_time == cur_time) { if(!allowEqual) { next_time = next_time.addSecs(period * PERIOD_UNIT); } } return next_time; }
重载文件。因重载的时刻可能恰好是可以触发任务的时刻,所以allowEqual=true:
entry.next_time = CalcNextTime(entry.start_time, entry.period, true); //... taskMap[entry.name] = entry;
轮询。在当前时刻已经可以触发的情况下,设allowEqual=false,得到下一个时刻:
TIME_ENTRY &entry = this->m_taskMap[name]; if(cur_time >= entry.next_time) { entry.next_time = CalcNextTime(entry.next_time, entry.period, false); StartProgram(entry); }
P.S.
开发过程是先开发的轮询,再加入重载。
轮询主体流程较为简单,但加入重载机制后,CalcNextTime()需要重新设计,于是才引入了bool allowEqual参数。
自测较为繁琐,好多时间是在傻等。