基础
让我们从一个简化的示例开始,并研究相关的Boost.Asio片段:
void handle_async_receive(...) { ... }
void print() { ... }
...
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
...
io_service.post(&print); // 1
socket.connect(endpoint); // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print); // 4
io_service.run(); // 5
什么是处理程序?
一个处理程序只不过是一个回调多。在示例代码中,有3个处理程序:
的print处理程序(1)。
的handle_async_receive处理程序(3)。
的print处理程序(4)。
即使print()两次使用相同的功能,每次使用也被视为创建自己的唯一可识别的处理程序。处理程序可以有多种形状和大小,范围从上述基本功能到更复杂的构造,例如从boost::bind()和lambda 生成的函子。不管复杂程度如何,处理程序仍然只不过是回调。
什么是工作?
工作是Boost.Asio代表应用程序代码执行的一些处理。有时,Boost.Asio可能会在得知某项工作后立即开始进行某些工作,而有时它可能会等待稍后的工作。完成工作后,Boost.Asio将通过调用提供的处理程序来通知应用程序。
Boost.Asio的保证了处理器将只当前调用线程中运行run(),run_one(),poll(),或poll_one()。这些是将起作用并调用处理程序的线程。因此,在上面的示例中,print()将其发布到io_service(1)中时不会被调用。而是将其添加到中,io_service并在以后的某个时间点调用。在这种情况下,它在io_service.run()(5)之内。
什么是异步操作?
一个异步操作创建工作,Boost.Asio的将调用处理程序通知应用程序时的工作已经完成。异步操作是通过调用名称带有前缀的函数来创建的async_。这些功能也称为启动功能。
异步操作可以分解为三个独特的步骤:
启动或通知相关的io_service工作需要完成。该async_receive操作(3)通知io_service,它需要从套接字异步读取数据,然后async_receive立即返回。
做实际的工作。在这种情况下,当socket接收数据时,字节将被读取并复制到中buffer。实际工作将通过以下任一方式完成:
初始化函数(3),如果Boost.Asio可以确定它不会阻塞。
当应用程序显式运行时io_service(5)。
调用handle_async_receive ReadHandler。同样,处理程序仅在运行的线程内调用io_service。因此,无论工作何时完成(3或5),都可以保证handle_async_receive()仅在io_service.run()(5)内调用。
这三个步骤之间在时间和空间上的分离称为控制流反转。这是使异步编程变得困难的复杂性之一。但是,有些技术可以帮助减轻这种情况,例如使用协程。
怎么io_service.run()办?
当线程调用时io_service.run(),将从该线程内调用工作和处理程序。在上面的示例中,io_service.run()(5)将阻塞直到:
它已从两个print处理程序中调用并返回,接收操作成功或失败,并且handle_async_receive已调用并返回了其处理程序。
通过io_service明确停止了io_service::stop()。
从处理程序中引发异常。
一种可能的伪装流可描述如下:
创建io_service
创建套接字
将打印处理程序添加到io_service(1)
等待套接字连接(2)
向io_service添加异步读取工作请求(3)
将打印处理程序添加到io_service(4)
运行io_service(5)
有工作或管理人员吗?
是的,有1个工作和2个处理程序
套接字有数据吗?不,什么都不做
运行打印处理程序(1)
有工作或管理人员吗?
是的,有1个工作和1个处理程序
套接字有数据吗?不,什么都不做
运行打印处理程序(4)
有工作或管理人员吗?
是的,有1件作品
套接字有数据吗?不,继续等待
-套接字接收数据-
套接字有数据,将其读入缓冲区
将handle_async_receive处理程序添加到io_service
有工作或管理人员吗?
是的,有一个处理程序
运行handle_async_receive处理程序(3)
有工作或管理人员吗?
否,将io_service设置为stopped然后返回
请注意如何当读完成后,它增加了一个处理程序的io_service。这个微妙的细节是异步编程的重要功能。它允许将处理程序链接在一起。例如,如果handle_async_receive没有获得它期望的所有数据,则其实现可能会发布另一个异步读取操作,从而导致io_service工作量增加,因此不会从返回io_service.run()。
请注意,当io_service已经跑出来的工作,应用程序必须reset()在io_service再次运行它之前。
示例问题和示例3a代码
现在,让我们检查问题中引用的两段代码。
问题代码
socket->async_receive将工作添加到io_service。因此,io_service->run()将阻塞直到读取操作成功完成或出现错误,并且ClientReceiveEvent已完成运行或引发异常。
示例3a代码
为了使读者更容易理解,下面是一个较小的带注释的示例3a:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<:asio::io_service::work> work = // '. 1
boost::in_place(boost::ref(io_service)); // .'
boost::thread_group worker_threads; // -.
for(int x = 0; x < 2; ++x) // :
{ // '.
worker_threads.create_thread( // :- 2
boost::bind(&boost::asio::io_service::run, &io_service) // .'
); // :
} // -'
io_service.post(boost::bind(CalculateFib, 3)); // '.
io_service.post(boost::bind(CalculateFib, 4)); // :- 3
io_service.post(boost::bind(CalculateFib, 5)); // .'
work = boost::none; // 4
worker_threads.join_all(); // 5
}
在较高级别,程序将创建2个线程来处理io_service的事件循环(2)。这样就产生了一个简单的线程池,该线程池将计算斐波纳契数(3)。
问题代码和此代码之间的主要区别在于,此代码在实际工作和处理程序添加到(3)之前调用io_service::run()(2 )。为了防止立刻返回,创建了一个对象(1)。这个对象可以防止工作用尽。因此,不会由于没有工作而返回。io_serviceio_service::run()io_service::workio_serviceio_service::run()
总体流程如下:
创建并添加添加到中的io_service::work对象io_service。
创建了调用的线程池io_service::run()。这些工作线程将不会io_service由于io_service::work对象而从中返回。
将3个计算斐波纳契数的处理程序添加到中io_service,并立即返回。工作线程而不是主线程可以立即开始运行这些处理程序。
删除io_service::work对象。
等待工作线程完成运行。这将仅在所有3个处理程序完成执行后发生,因为这三个处理程序都io_service没有工作。
可以用与原始代码相同的方式来编写不同的代码,在原始代码中,将处理程序添加到中io_service,然后io_service处理事件循环。这样就无需使用io_service::work,从而产生以下代码:
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3)); // '.
io_service.post(boost::bind(CalculateFib, 4)); // :- 3
io_service.post(boost::bind(CalculateFib, 5)); // .'
boost::thread_group worker_threads; // -.
for(int x = 0; x < 2; ++x) // :
{ // '.
worker_threads.create_thread( // :- 2
boost::bind(&boost::asio::io_service::run, &io_service) // .'
); // :
} // -'
worker_threads.join_all(); // 5
}
同步与异步
尽管问题中的代码使用异步操作,但是由于它正在等待异步操作完成,因此它可以有效地同步运行:
socket.async_receive(buffer, handler)
io_service.run();
等效于:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
作为一般经验法则,请尝试避免混合使用同步和异步操作。通常,它可以将一个复杂的系统变成一个复杂的系统。该答案强调了异步编程的优点,Boost.Asio 文档中也介绍了其中的一些优点。