问题描述
我正在尝试找出用于定期任务的简单调度程序.这个想法是提供一种方法来调度std::function<void()>
在任何给定时间间隔内的周期性执行,该时间间隔将是一秒的倍数.我试图使用boost :: asio编写它,但是到目前为止,我最终遇到了奇怪的行为-只有两个计划任务中的一个正在重复执行,但是没有遵循时间间隔.
I'm trying to figure out a simple scheduler for periodic tasks. The idea is to provide a mean to schedule periodic execution of std::function<void()>
with any given time interval which would be a multiplication of a second. I was trying to write it with the use of boost::asio, but so far I end up with strange behaviour - only one of two scheduled tasks is being repeatedly executed, but it don't follow the interval.
这是代码:
#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;
}
有人知道什么可能导致此问题吗?还是碰巧知道解决问题的更好方法?
Does anyone know what could cause the problem ? Or happen to know better approach to the problem ?
非常感谢!
推荐答案
分析
罪魁祸首似乎是在非标准的for each (auto task in tasks)
(Microsoft扩展名)中,它基本上等同于for (auto task : tasks)
.这意味着您在迭代tasks
向量的元素时会对其进行复制,然后在循环体内使用该副本.
Analysis
The main culprit seems to be in the non-standard for each (auto task in tasks)
(a Microsoft extension), which is basically equivalent of for (auto task : tasks)
. This means that you copy the elements of the tasks
vector as you iterate over them, and work with the copy inside the loop body.
这在PeriodicTask::execute
中变得尤为重要,尤其是在
This becomes relevant in PeriodicTask::execute
, specifically in
timer->async_wait(boost::bind(&PeriodicTask::execute, this));
其中this
指向上述副本,而不是向量中存储的对象.
where this
points to the aforementioned copy, not the object stored in the vector.
我们可以添加一些简单的调试跟踪,以打印矢量中对象的地址以及在其上调用execute
的对象的地址.还要在vector
中保留一些空间,这样就不会发生重新分配以简化操作的情况.
We can add some simple debugging traces, to print the address of the objects in the vector as well as the address of the object on which execute
is being invoked. Also reserve some space in the vector
, so that no reallocations happen to simplify things.
运行它时,我们将在控制台中看到以下内容:
When we run it, we'll see something like this in the console:
>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 是指开始时间.
Let's analyse it a little. Let us assume that t refers to the starting time.
- 第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
.
- Line 1: Created CPU timer @ address 22201304, set to expire at t + 5 seconds.
- Line 2: Created Memory timer @ address 22201332, set to expire at t + 10 seconds.
- Lines 3,4: Made copy of CPU timer to address 19922484. Ran the handler. Scheduled CPU timer to run
execute
on object at address 19922484 at t + 5 + 5 seconds. - Lines 5,6: Made copy of Memory timer to address 19922484. Ran the handler. Scheduled Memory timer to run
execute
on object at address 19922484 in t + 10 + 10 seconds.
在此阶段,我们有两个待处理的计时器,其中一个在十分之一秒内,一个在启动后20秒内.两者都计划在地址为 19922484 的对象上运行成员函数execute
,该对象当时不再存在(这在for循环中是临时的).偶然地,内存中仍然包含来自占据该位置的最后一个对象的数据,即内存"任务的副本.
At this stage, we have two timers pending, one in 10 seconds, one in 20 seconds from startup. Both of them are scheduled to run member function execute
on an object at address 19922484, which doesn't exist anymore at that point (it was a temporary in the for loop). By chance, the memory still contains the data from the last object that occupied that location -- the copy of the Memory task.
时间流逝...
- 7,8行:CPU计时器触发,并在地址 19922484 的对象上运行
execute
.如上所述,这意味着该方法在Memory任务副本的上下文中运行.因此,我们看到内存使用情况"已打印.
- Lines 7,8: The CPU timer fires, and runs
execute
on object at address 19922484. As explained above, this means the method is running in context of the copy of the Memory task. Therefore we see "Memory usage" printed.
在这一点上,计时器被重新安排.由于我们的上下文,我们没有重新安排CPU计时器,而是重新安排了尚待处理的Memory计时器.这将导致挂起的异步等待操作被取消,从而导致到期处理程序被调用并传递错误代码boost::asio::error::operation_aborted
.但是,到期处理程序将忽略错误代码.因此
At this point, a timer is rescheduled. Due to our context, instead of rescheduling the CPU timer, we reschedule the still pending Memory timer. This causes the pending asynchronous wait operation to be cancelled, which will in turn result in the expiration handler being called and passed the error code boost::asio::error::operation_aborted
. Your expiration handler, however, ignores the error codes. Thus
-
第9,10行:取消触发Memory计时器到期处理程序,
execute
在地址为 19922484 的对象上运行.如上所述,这意味着该方法在Memory任务副本的上下文中运行.因此,我们看到内存使用情况"已打印.内存计时器上已经有一个待处理的异步等待,因此我们在重新计划时会导致另一个取消.
Lines 9,10: Cancellation triggers the Memory timer expiration handler,
execute
runs on object at address 19922484. As explained above, this means the method is running in context of the copy of the Memory task. Therefore we see "Memory usage" printed. There is already a pending asynchronous wait on the Memory timer, so we cause another cancellation when rescheduling.
第11,12行:取消...您了解了要点.
Lines 11,12: Cancellation ... you get the gist.
更改for循环以使用引用.
Change the for loop to use a reference.
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
进一步分析
我们已经解决了一个小问题,但是您提供的代码还是有其他一些或多或少严重的问题.
Further Analysis
We have fixed one small problem, however there are several other more or less serious issues with the code you presented.
不好的一个地方是,您用一个已经存在的io_service
实例(PeriodicScheduler
的成员)的地址来初始化std::shared_ptr<boost::asio::io_service>
.
A bad one is that you initialize a std::shared_ptr<boost::asio::io_service>
with an address to already existing io_service
instance (the member of 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个彼此不认识的所有者.
which creates 3 owners of that object that don't know about each other.
类PeriodicTask
不应是可复制的-这没有意义,并且可以避免上面解决的主要问题.我的猜测是,其中的那些共享指针是试图解决被复制(并且io_service
本身不可复制)的问题.
Class PeriodicTask
shouldn't be copyable -- it doesn't make sense, and would avoid the primary problem solved above. My guess would be that those shared pointers in it were an attempt to solve a problem with it being copied (and io_service
being non-copyable itself).
最后,计时器的完成处理程序应具有boost::system::error_code const&
参数,至少应正确处理取消操作.
Finally, the completion handler for the timer should have a boost::system::error_code const&
parameter and at the least handle cancellation correctly.
让我们先从包含功能和一些便捷的日志记录功能开始.
Let's start with includes, and a little convenience logging function.
#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()
执行.最后,让我们修改完成处理程序以处理错误状态,并在取消后正确运行.
Next, let's make PeriodicTask
explicitly non-copyable and hold a reference to the io_service
instance. This means we can avoid the other shared pointers as well. We can write a separate method to start
the timer first time, and post it on the io_service
, so that it's executed by run()
. Finally, let's modify our completion handler to handle the error states, and behave correctly when cancelled.
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
方法.最后,让我们也使其不可复制,因为复制它实际上没有多大意义.
Let's have PeriodicScheduler
keep a vector of unique_ptr<PeriodicTask>
. Since PeriodicTask
now handles getting started itself, we can simplify the run
method. Finally, let's also make it non-copyable, since copying it doesn't really make much sense.
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;
};
现在,让我们将它们放在一起并尝试一下.
Now, let's put it all together and try it out.
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;
}
控制台输出:
Console output:
>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
这篇关于Boost-定期任务计划程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!