分析
罪魁祸首似乎在于非标准的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。但是,您的过期处理程序会忽略错误代码。因此
简单修复
更改 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<boost::asio::io_service>。
代码本质上是这样的:
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