基础
让我们从一个简化的示例开始,并研究相关的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);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
什么是处理程序?
一个处理程序无非是一个回调。在示例代码中,有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()
,将从该线程内调用work和handlers。在上面的示例中,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:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
在较高级别,程序将创建2个线程来处理io_service
的事件循环(2)。这样就产生了一个简单的线程池,该线程池将计算斐波纳契数(3)。
问题代码与此代码之间的主要区别在于,该代码在实际工作和处理程序添加到(3)之前调用io_service::run()
(2 )。为了防止立刻返回,创建了一个对象(1)。这个对象可以防止工作用尽。因此,不会由于没有工作而返回。io_service
io_service::run()
io_service::work
io_service
io_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));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
同步与异步
尽管问题中的代码正在使用异步操作,但是由于它正在等待异步操作完成,因此它可以有效地同步运行:
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文档中也介绍了其中的一些优点。