Boost - 周期性任务调度程序

时间:2016-11-02 14:00:27

标签: c++ multithreading c++11 boost boost-asio

我试图为周期性任务找出一个简单的调度程序。我们的想法是提供一个方法来安排std::function<void()>的定期执行,任何给定的时间间隔都是一秒的乘法。我试图使用boost :: asio来编写它,但到目前为止我最终会遇到奇怪的行为 - 只有两个计划任务中的一个被重复执行,但它不会遵循这个间隔。

以下是代码:

#include <functional>
#include <iostream>

#include <boost/asio.hpp>
#include <boost/bind.hpp>

class PeriodicTask
{
public: 
     PeriodicTask(boost::asio::io_service * ioService, int interval, std::function<void()> task)
     : ioService(ioService), 
       interval(interval), 
       task(std::make_shared<std::function<void()>>(task)),
       timer(std::make_shared<boost::asio::deadline_timer>(*ioService, boost::posix_time::seconds(interval)))
    {}

    void execute()
    {
        task->operator()();
        timer->expires_at(timer->expires_at() + boost::posix_time::seconds(interval));
        timer->async_wait(boost::bind(&PeriodicTask::execute,this));
    }

private:
     std::shared_ptr<boost::asio::io_service> ioService;
     std::shared_ptr<boost::asio::deadline_timer> timer;
     std::shared_ptr<std::function<void()>> task;
     int interval;
};

class PeriodicScheduler
{
public:
    void run()
    {
        for each (auto task in tasks)
        {
            task.execute();
        }
        io_service.run();
    }
    void  addTask(std::function<void()> task, int interval)
    {
        tasks.push_back(PeriodicTask(&io_service, interval, task));
    }
    boost::asio::io_service io_service;

private:
    std::vector<PeriodicTask> tasks;
};


void printCPUUsage()
{
    std::cout << "CPU usage: " << std::endl;
}

void printMemoryUsage()
{
    std::cout << "CPU usage: " << std::endl;
}
int _tmain(int argc, _TCHAR* argv[])
{   
    PeriodicScheduler scheduler;

    scheduler.addTask(printCPUUsage, 5);
    scheduler.addTask(printMemoryUsage, 10);

    scheduler.run();

    return 0;
}

有谁知道可能导致问题的原因?或碰巧知道更好的方法来解决问题?

非常感谢!

1 个答案:

答案 0 :(得分:6)

分析

主要罪魁祸首似乎是在非标准for each (auto task in tasks)(微软扩展)中,它基本上等同于for (auto task : tasks)。这意味着您在迭代它们时复制tasks向量的元素,并使用循环体内的副本。

这在PeriodicTask::execute中变得相关,特别是在

timer->async_wait(boost::bind(&PeriodicTask::execute, this));

其中this指向上述副本,而不是存储在向量中的对象。

我们可以添加一些简单的调试跟踪,以打印向量中对象的地址以及调用execute的对象的地址。同时在vector中保留一些空间,这样就不会发生重新分配以简化操作。

当我们运行它时,我们会在控制台中看到类似的内容:

>example.exe
02-11-2016 20-04-36 created this=22201304
02-11-2016 20-04-36 created this=22201332
02-11-2016 20-04-36 execute this=19922484
02-11-2016 20-04-36 CPU usage
02-11-2016 20-04-36 execute this=19922484
02-11-2016 20-04-36 Memory usage
02-11-2016 20-04-46 execute this=19922484
02-11-2016 20-04-46 Memory usage
02-11-2016 20-04-46 execute this=19922484
02-11-2016 20-04-46 Memory usage
02-11-2016 20-04-46 execute this=19922484
02-11-2016 20-04-46 Memory usage
02-11-2016 20-04-46 execute this=19922484
.... and so on and on and on....

让我们稍微分析一下。我们假设 t 指的是开始时间。

  • 第1行:创建的CPU计时器@地址 22201304 ,设置为 t + 5秒
  • 第2行:已创建内存计时器@地址 22201332 ,设置为 t + 10秒
  • 第3,4行:制作CPU计时器副本以解决 19922484 。跑到处理程序。计划的CPU计时器在地址 19922484 的对象上运行execute t + 5 + 5秒
  • 第5,6行:制作内存计时器副本以解决 19922484 。跑到处理程序。预定内存计时器在地址 19922484 上的对象上运行execute t + 10 + 10秒

在这个阶段,我们有两个待定的计时器,一个在10秒内,一个在20秒内启动。它们都被安排在地址 19922484 的对象上运行成员函数execute,该对象在那时不再存在(它在for循环中是临时的)。偶然地,内存仍然包含来自占用该位置的最后一个对象的数据 - 内存任务的副本。

时间流逝......

  • 第7,8行:CPU计时器触发,并在地址 19922484 的对象上运行execute。如上所述,这意味着该方法在Memory任务的副本的上下文中运行。因此,我们看到“内存使用情况”已打印。

此时,重新安排计时器。由于我们的上下文,我们重新安排仍然挂起的内存计时器,而不是重新安排CPU计时器。这会导致挂起的异步等待操作被取消,这将导致调用过期处理程序并传递错误代码boost::asio::error::operation_aborted。但是,您的到期处理程序会忽略错误代码。因此

  • 第9,10行:取消触发内存计时器到期处理程序,execute在地址 19922484 的对象上运行。如上所述,这意味着该方法在Memory任务的副本的上下文中运行。因此我们看到“内存使用”打印出来。内存计时器上已经有待处理的异步等待,因此重新安排时会导致另一次取消。

  • 第11,12行:取消......你得到了要点。

简单修复

更改for循环以使用参考。

for (auto& task : tasks) {
    // ....
}

控制台输出:

>so02.exe
02-11-2016 20-39-30 created this=19628176
02-11-2016 20-39-30 created this=19628204
02-11-2016 20-39-30 execute this=19628176
02-11-2016 20-39-30 CPU usage
02-11-2016 20-39-30 execute this=19628204
02-11-2016 20-39-30 Memory usage
02-11-2016 20-39-40 execute this=19628176
02-11-2016 20-39-40 CPU usage
02-11-2016 20-39-45 execute this=19628176
02-11-2016 20-39-45 CPU usage
02-11-2016 20-39-50 execute this=19628176
02-11-2016 20-39-50 CPU usage
02-11-2016 20-39-50 execute this=19628204
02-11-2016 20-39-50 Memory usage
02-11-2016 20-39-55 execute this=19628176
02-11-2016 20-39-55 CPU usage

进一步分析

我们已经解决了一个小问题,但是您提供的代码还存在其他一些或多或少的严重问题。

糟糕的是,您使用已存在的std::shared_ptr<boost::asio::io_service>实例(io_service成员)的地址初始化PeriodicScheduler

代码本质上就像:

boost::asio::io_service io_service;
std::shared_ptr<boost::asio::io_service> ptr1(&io_service);
std::shared_ptr<boost::asio::io_service> ptr2(&io_service);

创建该对象的3个所有者,彼此不了解。

PeriodicTask不应该是可复制的 - 它没有意义,并且会避免上面解决的主要问题。我的猜测是,它中的那些共享指针试图解决它被复制的问题(并且io_service本身是不可复制的。)

最后,计时器的完成处理程序应该有一个boost::system::error_code const&参数,并且至少可以正确取消句柄。

完整解决方案

让我们从包含开始,以及一点便利记录功能。

#include <ctime>
#include <iostream>
#include <iomanip>
#include <functional>

#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/noncopyable.hpp>

void log_text(std::string const& text)
{
    auto t = std::time(nullptr);
    auto tm = *std::localtime(&t);
    std::cout << std::put_time(&tm, "%d-%m-%Y %H-%M-%S") << " " << text << std::endl;
}

接下来,让PeriodicTask显式不可复制,并保留对io_service实例的引用。这意味着我们也可以避免使用其他共享指针。我们可以第一次向start计时器编写一个单独的方法,并将其发布在io_service上,以便由run()执行。最后,让我们修改完成处理程序来处理错误状态,并在取消时正确运行。

class PeriodicTask : boost::noncopyable
{
public:
    typedef std::function<void()> handler_fn;

    PeriodicTask(boost::asio::io_service& ioService
        , std::string const& name
        , int interval
        , handler_fn task)
        : ioService(ioService)
        , interval(interval)
        , task(task)
        , name(name)
        , timer(ioService)
    {
        log_text("Create PeriodicTask '" + name + "'");
        // Schedule start to be ran by the io_service
        ioService.post(boost::bind(&PeriodicTask::start, this));
    }

    void execute(boost::system::error_code const& e)
    {
        if (e != boost::asio::error::operation_aborted) {
            log_text("Execute PeriodicTask '" + name + "'");

            task();

            timer.expires_at(timer.expires_at() + boost::posix_time::seconds(interval));
            start_wait();
        }
    }

    void start()
    {
        log_text("Start PeriodicTask '" + name + "'");

        // Uncomment if you want to call the handler on startup (i.e. at time 0)
        // task();

        timer.expires_from_now(boost::posix_time::seconds(interval));
        start_wait();
    }

private:
    void start_wait()
    {
        timer.async_wait(boost::bind(&PeriodicTask::execute
            , this
            , boost::asio::placeholders::error));
    }

private:
    boost::asio::io_service& ioService;
    boost::asio::deadline_timer timer;
    handler_fn task;
    std::string name;
    int interval;
};

让我们PeriodicScheduler保持unique_ptr<PeriodicTask>的向量。由于PeriodicTask现在处理自己的入门,我们可以简化run方法。最后,让我们也让它不可复制,因为复制它并没有多大意义。

class PeriodicScheduler : boost::noncopyable
{
public:
    void run()
    {
        io_service.run();
    }

    void addTask(std::string const& name
        , PeriodicTask::handler_fn const& task
        , int interval)
    {
        tasks.push_back(std::make_unique<PeriodicTask>(std::ref(io_service)
            , name, interval, task));
    }

private:
    boost::asio::io_service io_service;
    std::vector<std::unique_ptr<PeriodicTask>> tasks;
};

现在,让我们把它们放在一起然后尝试一下。

int main()
{
    PeriodicScheduler scheduler;

    scheduler.addTask("CPU", boost::bind(log_text, "* CPU USAGE"), 5);
    scheduler.addTask("Memory", boost::bind(log_text, "* MEMORY USAGE"), 10);

    log_text("Start io_service");

    scheduler.run();

    return 0;
}

控制台输出:

>example.exe
02-11-2016 19-20-42 Create PeriodicTask 'CPU'
02-11-2016 19-20-42 Create PeriodicTask 'Memory'
02-11-2016 19-20-42 Start io_service
02-11-2016 19-20-42 Start PeriodicTask 'CPU'
02-11-2016 19-20-42 Start PeriodicTask 'Memory'
02-11-2016 19-20-47 Execute PeriodicTask 'CPU'
02-11-2016 19-20-47 * CPU USAGE
02-11-2016 19-20-52 Execute PeriodicTask 'CPU'
02-11-2016 19-20-52 * CPU USAGE
02-11-2016 19-20-52 Execute PeriodicTask 'Memory'
02-11-2016 19-20-52 * MEMORY USAGE
02-11-2016 19-20-57 Execute PeriodicTask 'CPU'
02-11-2016 19-20-57 * CPU USAGE
02-11-2016 19-21-02 Execute PeriodicTask 'CPU'
02-11-2016 19-21-02 * CPU USAGE
02-11-2016 19-21-02 Execute PeriodicTask 'Memory'
02-11-2016 19-21-02 * MEMORY USAGE