Qt 定时调度

需求:后台常驻进程中实现定时调度(计划任务,每隔若干分钟执行任务)。

可模拟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;
            }
        }
    }
}
View Code

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;
}
View Code

重载文件。因重载的时刻可能恰好是可以触发任务的时刻,所以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参数。

自测较为繁琐,好多时间是在傻等。

原文地址:https://www.cnblogs.com/daxia319/p/4797463.html