【问题标题】:Boost - periodic task schedulerBoost - 定期任务调度器
【发布时间】:2017-03-15 21:03:02
【问题描述】:

我正在尝试为周期性任务找出一个简单的调度程序。这个想法是提供一种方法来安排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;
}

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

非常感谢!

【问题讨论】:

  • 一个明显错误的事情是你如何用已经存在的io_service 实例(PeriodicScheduler 的成员)的地址初始化一个std::shared_ptr&lt;boost::asio::io_service&gt;PeriodicTask::ioService 应该只是一个参考。另外两个共享指针似乎也没有必要。另一方面,我可能会让PeriodicSheduler 保留std::vector&lt;std::shared_ptr&lt;PeriodicTask&gt;&gt;,并使PeriodicTask 不可复制。

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


【解决方案1】:

分析

罪魁祸首似乎在于非标准的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 计时器在 t + 5 + 5 秒时在地址 19922484 的对象上运行 execute
  • 第 5,6 行:将内存计时器复制到地址 19922484。跑了处理程序。计划内存计时器在 t + 10 + 10 秒内对地址 19922484 的对象运行 execute

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

时间流逝……

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

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

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

  • 第 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

进一步分析

我们已经修复了一个小问题,但是您提供的代码还有其他几个或多或少严重的问题。

一个糟糕的问题是,您使用已经存在的io_service 实例(PeriodicScheduler 的成员)的地址来初始化std::shared_ptr&lt;boost::asio::io_service&gt;

代码本质上是这样的:

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&amp; 参数,并且至少可以正确处理取消。

完整的解决方案

让我们从包含和一些方便的日志功能开始。

#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&lt;PeriodicTask&gt; 的向量。由于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

【讨论】:

    猜你喜欢
    • 2019-05-30
    • 2016-08-23
    • 1970-01-01
    • 1970-01-01
    • 2012-02-03
    • 1970-01-01
    • 2012-10-06
    • 1970-01-01
    • 2011-07-02
    相关资源
    最近更新 更多