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

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

如何实现定时任务呢?如果不用任何技巧,我们可以把当前线程睡一睡,睡到特定的时间点再起来执行特定任务。看起来是解决了这个问题,但是,如果在睡一睡的这个过程,我还想执行一些其他的任务,怎么办?好像也可以解决,开一条新的线程去睡;如果在睡的过程中我不想再执行这个任务了,怎么办,直接中断线程?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);
            else
                nullptr;
        }
    }

    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);
        else
            nullptr;
    }
}

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秒。

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

其他

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

测试

这份代码我分别在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;
}

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


参考:

发布了299 篇原创文章 · 获赞 353 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/FlushHip/article/details/83542815