大多数程序以某种方式与外界交互,无论是通过文件、网络、串行电缆还是控制台。 有时,就像网络一样,单个 I/O 操作可能需要很长时间才能完成。 这对应用程序开发提出了特殊的挑战。
Boost.Asio 提供了管理这些长时间运行的操作的工具,而无需程序使用基于线程和显式加锁的并发模型。
Boost.Asio 库适用于使用 C++ 进行系统编程的程序员,这些程序员通常需要访问操作系统功能,例如网络。 特别是,Boost.Asio 解决了以下目标:
尽管 Boost.Asio 一开始主要关注网络,但它的异步 I/O 概念已经扩展到包括其他操作系统资源,例如串行端口、文件描述符等。
Asio可用于对IO对象(如套接字)执行同步和异步操作。在使用 Boost.Asio 之前,了解一下 Boost.Asio 的各个部分、您的程序以及它们如何协同工作的概念图可能会很有用。
作为入门级的示例,让我们考虑在套接字上执行连接操作时会发生什么。 我们将从检查同步操作开始。
你的程序中应该至少有一个IO执行上下文(I/O Execution Context),例如一个boost::asio::io_context对象,boost::asio::thread_pool对象或者boost::asio::system_context对象。I/O可执行上下文代表了你的程序与操作系统的I/O服务的链接。
boost::asio::io_context io_context;
为了执行I/O操作,你的程序需要一个I/O对象,例如TCP套接字:
boost::asio::ip::tcp::socket socket(io_context);
当执行一个同步的连接操作时,会发生下面的事件时序:
socket.connect(server_endpoint);
boost::system::error_code ec; socket.connect(server_endpoint,ec);
那么error_code类型变量ec将会被设置为运行结果,不会抛出异常。
当使用异步操作的时候,发生的事件时序有所不同。
socket.async_connect(server_endpoint,completion_handler);
completion_handler是一个函数并且函数签名如下:
void completion_handler(const boost::system::error_code ec);
所需的确切签名取决于正在执行的异步操作。参考文档指出了每个操作的适当形式。
Asio为同步操作和异步操作提供的并行支持。异步操作是基于Proactor设计模式。与只有同步和Reactor方式相比,这种模式的优缺点如下所述:
我们先了解一下Proactor在Asio中是如何实现的,但不设计具体的细节。
在许多平台,Asio使用Reactor实现Proactor设计模式,例如select,epoll或者kqueue。这种实现方法对应的Proactor设计模式如下:
在Windows NT,2000和XP。Asio利用Overlapped I/O的优点提供了高效的Proactor设计模式的实现。这种实现方式对应的Proactor设计模式如下:
select()
) 来实现该库。程序复杂度。由于操作启动和完成之间的时间和空间分离,使用异步机制开发应用程序更加困难。 由于反向控制流,应用程序也可能更难调试。
内存利用率。在读或写操作期间必须提交缓冲区空间,这可能会无限期地持续下去,并且每个并发操作都需要一个单独的缓冲区。 另一方面,Ractor模式在套接字准备好读取或写入之前不需要缓冲区空间。
一般来说,并发使用不同的对象是安全的,但是并发使用单个对象是不安全的。但是,诸如io_context之类的类型提供了强力的保证,即并发使用单个对象是安全的。
多个线程可以调用 io_context::run() 来设置一个线程池,可以从中调用完成处理器。这种方法也可以与post()一起使用,作为跨线程池执行任意计算任务的方法。
请注意,所有加入io_context的线程都被认为是等价的,io_context可以以任意的方式在它们之间分发工作。
这个库对于特定平台的实现可以使用一个或多个内部线程来模拟异步性。这些线程对于库的使用者必须尽可能不可见。特别的,这些线程:
此方法由以下保证补充:
因此,库用户有责任创建和管理通知将发送到的所有线程。
采用这种方法的原因包括:
Strands被定义为事件处理程序的严格调用。使用Strands允许在多线程程序中执行代码而无需显式加锁。
Strands可以是显式的,也可以是隐式的。如下面的例子:
strand<>
或者io_context::strand
的实例。所有的事件处理器函数对象需要使用boost::asio::bind_executor
绑定到strand上,或者使用strand对象进行发布/调度。在异步操作组合的情况下,像async_read()
或者async_read_until()
,如果一个完成处理器经过了一个strand,那么所有的中间处理器也应该经过同样的strand。这是确保能够线程安全地访问调用者和组合操作之间共享对象所必要的(在async_read()
的情况下,它是套接字,调用者可以close()
取消操作)。
为了实现这一点,所有的异步操作通过使用get_associated_executor
函数获取处理器相关的执行器。例如:
boost::asio::associated_executor_t<Handler> a = boost::asio::get_associated_executor(h);
相关的执行器必须满足Executor要求。异步操作使用它来提交中间和最终处理器以供执行。
可以通过指定嵌套类型executor_type
和成员函数get_executor
为特定处理器类型自定义执行器:
class my_handler { public: //Executor 类型要求的自定义实现。 typedef my_executor executor_type; //返回一个自定义执行器的实现 executor_type get_executor() const noexcept { return my_executor(); } void operator()() {...} };
在更复杂的情况下,associated_executor
模板可能会直接部分特化:
//处理器 struct my_handler { void operator()() {...} }; namespace boost {namespace asio { //特化associator_executor模板 template<class Executor> struct associated_executor<my_handler,Executor> { //Executor 类型要求的自定义实现。 typedef my_executor type; //返回一个自定义执行器的实现 static type get(const my_handler&,const Executor&=Executor()) noexcept { return my_executor(); } }; }}
boost::asio::bind_executor()
函数用来将特定的executor对象(像strand)绑定到一个完成处理器上。这个绑定会自动关联一个执行器。例如,为了将strand绑定到一个完成处理器上,我们可以简单地写为:
my_socket.async_read_some(my_buffer, boost::asio::bind_executor(my_strand,[](error_code ec,size_t length) { //.... }));
从根本上说,I/O涉及在内存的连续区域(称为缓冲区)之间传输数据。
这些缓冲区可以简单地表示为由一个指针和一个字节大小组成的元组。但是,为了开发高效的网络应用程序,Asio包括对分散-聚集操作的支持。这些操作涉及一个或多个缓冲区。
因为,我们需要一个表示缓冲区集合的抽象。Asio使用的方法就是定义一个(实际上是两个)来表示单个缓冲区。这些可以存储在一个容器中,而该容器可以传递给分散-聚集操作。
除了将缓冲区指定为指针和字节大小之外,Boost.Asio 还区分了可修改内存(称为可变)和不可修改内存(后者是从 const 限定变量的存储中创建的)。 因此,这两种类型可以定义如下:
typedef std::pair<void*,std::size_t> mutable_buffer; typedef std::pair<const void*,std::size_t> const_buffer;
mutable_buffer可以转为const_buffer,但是不能反过来转换。
但是,Asio并没有使用上面的定义,而是定义了两个类mutable_buffer
和const_buffer
。其目的是提供连续内存的不透明表示,其中:
mutable_bufer
可以转为const_buffer
,但是不能反过来。boost::array
或std::vector
,或来自std::stirng
。data()
显式访问底层的内存。通常来说,应用程序不需要这样做,但是库的实现需要传递原始内存给底层操作系统函数。最后,多个buffer可以通过将buffer对象放入容器中传给分散-聚集操作(像read()
或write()
)。定义了MutableBufferSequence
和ConstBufferSequence
概念,以便可以使用像std::vector,std::list,std::array,boost::array
的容器。
类boost::asio::basic_streambuf
派生自std::basic_streambuf
以将输入序列和输出序列与某种字符数组类型的一个或多个对象相关联,这些对象的元素存储任意值。这些字符数组对象在 streambuf 对象内部,但提供了对数组元素的直接访问,以允许它们与 I/O 操作一起使用,例如套接字的发送或接收操作:
data()
成员函数访问。该函数的返回类型满足ConstBufferSequence
的要求。prepare()
成员函数访问。函数的返回类型满足MutableBufferSequence
要求。commit()
成员函数,数据从输出序列的前面传输到输入序列的后面。consume()
成员函数从输入序列的前面删除数据。streambuf 构造函数接受一个size_t
参数,指定输入序列和输出序列的大小之和的最大值。 如果成功,任何将内部数据增长超过此限制的操作都将抛出 std::length_error
异常。
buffers_iterator<>
类模板允许遍历缓冲区序列(即满足 MutableBufferSequence
或ConstBufferSequence
要求的类型),就好像它们是连续的字节序列一样。还提供了称为 buffers_begin()
和 buffers_end()
的辅助函数,其中会自动推导出 buffers_iterator<>
模板参数。
举个例子,从套接字中读取一行放入std::string,可以写为:
boost::asio::streambuf sb; ... std::size_t n = boost::asio::read_until(sock,sb,'\n'); boost::asio::streambuf::const_buffers_type bufs = sb.data(); std::string line( boost::asio::buffers_begin(bufs), boost::asio::buffers_begin(bufs)+n );
一些标准库的实现,比如微软Visual c++ 8.0及更高版本附带的库,提供了一个称为迭代器调试的特性。这意味着在运行时检查迭代器的有效性。如果程序尝试使用已失效的迭代器,则会触发断言。 例如:
std::vector<int> v(1); std::vector<int>::iterator i = v.begin(); v.clear(); //使迭代器无效 *i=0; //断言
Asio利用了这一特性,加入到了buffer的debugging。考虑下面的代码:
void dont_do_this() { std::string msg = "Hello,world!"; boost::asio::async_write(sock,boost::asio::buffer(msg),my_handler); }
当您调用异步读取或写入时,您需要确保操作的缓冲区在调用完成处理器之前有效。在上面的例子中,缓冲区是 std::string 变量 msg。这个变量在堆栈上,所以它在异步操作完成之前就超出了范围。如果你很幸运,那么应用程序会崩溃,但更有可能出现随机故障。
启用缓冲区调试时,Asio 将迭代器存储到string中,直到异步操作完成,然后解引用它以检查其有效性。在上面的示例中,您将在 Asio 尝试调用完成处理器之前观察到断言失败。
当定义_GLIBCXX_DEBUG
时,此功能会自动适用于 Microsoft Visual Studio 8.0 或更高版本以及 GCC。此检查会产生性能成本,因此缓冲区调试仅在调试版本中启用。对于其他编译器,它可以通过定义 BOOST_ASIO_ENABLE_BUFFER_DEBUGGING
来启用。 它也可以通过定义 BOOST_ASIO_DISABLE_BUFFER_DEBUGGING
来显式禁用。
Asio的许多I/O对象是面向流。这就意味着:
提供面向流的 I/O 模型的对象具有以下一种或多种类型要求:
SyncReadStream
:其中使用名为read_some()
的成员函数执行同步读取操作。AsyncReadStream
,其中使用名为 async_read_some()
的成员函数执行异步读取操作。SyncWriteStream
,其中使用名为write_some()
的成员函数执行同步写入操作。AsyncWriteStream
,其中使用名为 async_write_some()
的成员函数执行异步写入操作。面向流的IO对象的例子包括ip::tcp::socket,ssl::stream<>,posix::stream_descriptor,windows::stream_handle
等等。
程序通常希望传输确切数量的字节。 当发生短读或短写时,程序必须重新开始操作,并继续这样做,直到传输了所需的字节数。 Asio 提供了自动执行此操作的通用函数:read()
、async_read()
、write()
和 async_write()
。
read、async_read、read_until
或 async_read_until
函数违反它们的约定。 例如。 由于 EOF,N 个字节的读取可能会提前完成。有时,程序必须与想要自己执行 I/O 操作的第三方库集成。为促进这一点,Asio 的同步和异步操作可用于等待套接字准备好读取、准备写入或具有挂起的错误条件。
举个例子,执行非阻塞读:
ip::tcp::socket socket(my_io_context); ... socket.non_blocking(true); ... socket.async_wait(ip::tcp::socket::wait_read,read_handler); ... void read_handler(boost::system::error_code ec) { if(!ec) { std::vector<charA> buf(socket.available()); socket.read_some(buffer(buf)); } }
所有平台上的套接字和 POSIX 面向流的描述符类都支持这些操作。
许多常用的 Internet 协议都是基于行的,这意味着它们具有由字符序列“\r\n”分隔的协议元素。例如HTTP,SMTP,FTP。为了更容易地实现基于行的协议以及其他使用分隔符的协议,Asio 提供了包括read_until()
和 async_read_until()
的函数。
下面例子说明了async_read_until()
在HTTP服务器中的使用,用来接收来自客户端的HTTP请求的第一行:
class http_connection { ... void start() { boost::asio::async_read_until(socket_,data_,"\r\n",boost::bind(&http_connection::handle_request_line,this,_1)); } void handle_request_line(boost::system::error_code ec) { if(!ec) { std::string method, uri, version; char sp1,sp2,cr,lf; std::istream is(&data_); is.unsetf(std::ios_base::skipws); is >> method >>sp1 >> uri >> sp2 >>version >> cr >> lf; ... } } ... boost::asio::ip::tcp::socket socket_; boost::asio::streambuf data_; };
streambuf 数据成员用作存储在搜索分隔符之前从套接字读取的数据的地方。重要的是要记住,分隔符之后可能还有其他数据。 这个多余的数据应该留在流缓冲中,以便后续调用read_until()
或 async_read_until()
可以检查它。
分隔符可以指定为单个char
、std::string
或 boost::regex
。 read_until()
和 async_read_until()
函数还包括接受称为匹配条件的用户定义函数对象的重载。 例如,要将数据读入流缓冲直到遇到空格:
typedef boost::asio::buffers_iterator<boost::asio::streambuf::const_buffers_type> iterator; std::pair<iterator,bool> match_whitespace(iterator begin,iterator end) { iterator i = begin; while(i!=end) { if(std::isspace(*i++)) return std::make_pair(i,true); return std::make_pair(i,false); } } ... boost::asio::streambuf b; boost::asio::read_until(s,b,match_whitespace);
将数据读入流缓冲区直到找到匹配的字符为止。
class match_char { public: explicit match_char(char c) : c_(c) {} template <typename Iterator> std::pair<Iterator,bool>operator()(Iterator begin,iterator end) const { Iterator i = begin; while(i != end) if(c_ == *i++) return std::make_pair(i,true); return std::make_pair(i,false); } private: char c_; }; namespace boost { namespace asio { template<> struct is_match_condition<match_char> : public boost::true_type {} } } .... boost::asio::streambuf b; boost::asio::read_until(s,b,match_char('a'));
对于函数和具有嵌套 result_type 类型定义的函数对象, is_match_condition<>
类型特征自动计算为真。对于其他类型,特征必须明确特化,如上所示。
许多同步操作需要申请一个对象来存储与操作相关的状态。例如,Win32 实现需要将 OVERLAPPED 派生对象传递给 Win32 API 函数。
并且,程序通常包含易于识别的同步操作链。半双工协议实现(例如 HTTP 服务器)每个客户端都有一个操作链(接收后发送)。一个全双工协议实现有两条并行执行的链。程序应该能够利用这些知识为链中的所有异步操作重用内存。
给定一个用户定义的Handler对象h的拷贝,如果实现需要分配与该Handler关联的内存,它将使用 get_related_allocator
函数获取分配器。例如:
boost::asio::associated_allocator_t<Handler> a = boost::asio::get_associated_allocator(h);
关联的分配器必须满足标准的分配器的标准。
默认情况下,处理器使用标准分配器(用::oprator new()
和::operator delete()
实现)。可以通过指定嵌套类型 allocator_type 和成员函数 get_allocator() 为特定处理其类型定制分配器:
class my_handler { public: // 分配器类型要求的自定义实现. typedef my_allocator allocator_type; // 返回一个自定义的分配器实现 allocator_type get_allocator() const noexcept { return my_allocator(); } void operator()() { ... } };
在大多数复杂长江下,会直接对associated_allocator
模板进行特化:
namespace boost { namespace asio { template <typename Allocator> struct associated_allocator<my_handler, Allocator> { // Custom implementation of Allocator type requirements. typedef my_allocator type; // Return a custom allocator implementation. static type get(const my_handler&, const Allocator& a = Allocator()) noexcept { return my_allocator(); } }; } } // namespace boost::asio
该实现保证释放将在调用关联的处理器之前发生,这意味着内存已准备好重新用于处理器启动的任何新异步操作。
可以从调用库函数的任何用户创建的线程调用自定义内存分配函数。 该实现保证,对于包含在库中的异步操作,该实现不会对该处理器的内存分配函数进行并发调用。 如果需要从不同线程调用分配函数,在实现时将插入适当的内存屏障以确保正确的内存可见性。
为了帮助调试异步程序,Asio提供了对Handler追踪的支持。通过定义BOOST_ASIO_ENABLE_HANDLER_TRACKING
开启,Asio将调试输出写入标准错误流。输出记录异步操作及其和Handler之间的关系。
此功能在调试时很有用,您需要知道异步操作是如何链接在一起的,或者挂起的异步操作是什么。下面是运行HTTP服务器示例时的输出,处理单个请求,然后通过Ctrl+C关闭:
@asio|1589424178.741850|0*1|signal_set@0x7ffee977d878.async_wait @asio|1589424178.742593|0*2|socket@0x7ffee977d8a8.async_accept @asio|1589424178.742619|.2|non_blocking_accept,ec=asio.system:11 @asio|1589424178.742625|0|resolver@0x7ffee977d760.cancel @asio|1589424195.830382|.2|non_blocking_accept,ec=system:0 @asio|1589424195.830413|>2|ec=system:0 @asio|1589424195.830473|2*3|socket@0x7fa71d808230.async_receive @asio|1589424195.830496|.3|non_blocking_recv,ec=system:0,bytes_transferred=151 @asio|1589424195.830503|2*4|socket@0x7ffee977d8a8.async_accept @asio|1589424195.830507|.4|non_blocking_accept,ec=asio.system:11 @asio|1589424195.830510|<2| @asio|1589424195.830529|>3|ec=system:0,bytes_transferred=151 @asio|1589424195.831143|3^5|in 'async_write' (./../../../boost/asio/impl/write.hpp:330) @asio|1589424195.831143|3*5|socket@0x7fa71d808230.async_send @asio|1589424195.831186|.5|non_blocking_send,ec=system:0,bytes_transferred=1090 @asio|1589424195.831194|<3| @asio|1589424195.831218|>5|ec=system:0,bytes_transferred=1090 @asio|1589424195.831263|5|socket@0x7fa71d808230.close @asio|1589424195.831298|<5| @asio|1589424199.793770|>1|ec=system:0,signal_number=2 @asio|1589424199.793781|1|socket@0x7ffee977d8a8.close @asio|1589424199.793809|<1| @asio|1589424199.793840|>4|ec=asio.system:125 @asio|1589424199.793854|<4| @asio|1589424199.793883|0|signal_set@0x7ffee977d878.cancel
每一行的格式如下:
<tag> | <timestamp> | <action> | <description>
<tag>总是@asio,用于从程序输出中识别和提取Handler追踪消息。
<timestamp>是距离1970.1.1的秒和毫秒。
<action>采取下面的形式之一:
io_context
被销毁时,任何未完成的异步操作通常都是这种情况。close()
和cancel()
操作会被记录,因为这些操作可能会影响挂起的异步操作的状态。<description> 显示同步或异步操作,格式为 <object-type>@<pointer>.<operation>。 对于处理程序条目,它显示了一个逗号分隔的参数列表及其值。
如上所示,每个处理程序都分配了一个数字标识符。 如果处理器跟踪输出显示处理程序编号为 0,则表示该操作是在任何处理器之外执行的。
程序可以通过在源代码中使用宏 BOOST_ASIO_HANDLER_LOCATION
来增加处理器跟踪输出的位置信息。 例如:
#define HANDLER_LOCATION \ BOOST_ASIO_HANDLER_LOCATION((__FILE__, __LINE__, __func__)) // ... void do_read() { HANDLER_LOCATION; auto self(shared_from_this()); socket_.async_read_some(boost::asio::buffer(data_, max_length), [this, self](boost::system::error_code ec, std::size_t length) { HANDLER_LOCATION; if (!ec) { do_write(length); } }); }
使用附加位置信息可用时,处理程序跟踪输出可能包括源位置的调用堆栈:
@asio|1589423304.861944|>7|ec=system:0,bytes_transferred=5 @asio|1589423304.861952|7^8|in 'async_write' (./../../../boost/asio/impl/write.hpp:330) @asio|1589423304.861952|7^8|called from 'do_write' (handler_tracking/async_tcp_echo_server.cpp:62) @asio|1589423304.861952|7^8|called from 'operator()' (handler_tracking/async_tcp_echo_server.cpp:51) @asio|1589423304.861952|7*8|socket@0x7ff61c008230.async_send @asio|1589423304.861975|.8|non_blocking_send,ec=system:0,bytes_transferred=5 @asio|1589423304.861980|<7|
此外,如果 std::source_location
或 std::experimental::source_location
可用,则 use_awaitable_t
标记(当默认构造或用作默认完成标记时)还将导致处理器跟踪为每个新创建的异步操作输出源位置 。 use_awaitable_t
对象也可以用局部信息显式构造。
可以使用包含的 handlerviz.pl 工具对处理器跟踪输出进行后处理,以创建处理程序的可视化表示(需要 GraphViz 工具dot
)。
可以通过将 BOOST_ASIO_CUSTOM_HANDLER_TRACKING
宏定义为头文件的名称(用“”或 <> 括起来)来自定义处理器跟踪。 此头文件必须实现以下预处理器宏:
Macro | Description |
---|---|
BOOST_ASIO_INHERIT_TRACKED_HANDLER |
为实现异步操作的类指定基类。 使用时,宏紧跟在类名之后,因此它必须具有以下形式:public my_class。 |
BOOST_ASIO_ALSO_INHERIT_TRACKED_HANDLER |
为实现异步操作的类指定基类。 使用时,宏跟随其他基类,因此它必须具有形式,public my_class。 |
BOOST_ASIO_HANDLER_TRACKING_INIT(args) |
用于初始化跟踪机制的表达式。 |
BOOST_ASIO_HANDLER_LOCATION(args) |
用于定义源代码位置的变量声明。 args 是一个带括号的函数参数列表,包含文件名、行号和函数名。 |
BOOST_ASIO_HANDLER_CREATION(args) |
在创建异步操作时调用的N表达式。Args是一个带圆括号的函数参数列表,包含拥有的执行上下文、被跟踪的处理程序、对象类型的名称、对象的指针、对象的本机句柄和操作名称。 |
BOOST_ASIO_HANDLER_COMPLETION(args) |
在异步操作完成时调用的表达式。 args 是包含跟踪处理程序的带括号的函数参数列表。 |
BOOST_ASIO_HANDLER_INVOCATION_BEGIN(args) |
在调用完成处理程序之前立即调用的表达式。 args 是一个带括号的函数参数列表,包含完成处理程序的参数。 |
BOOST_ASIO_HANDLER_INVOCATION_END |
在调用完成处理程序后立即调用的表达式。 |
BOOST_ASIO_HANDLER_OPERATION(args) |
在调用某些同步对象操作(例如 close() 或 cancel())时调用的表达式。 args 是一个带括号的函数参数列表,包含拥有的执行上下文、对象类型的名称、指向对象的指针、对象的本机句柄和操作名称。 |
BOOST_ASIO_HANDLER_REACTOR_REGISTRATION(args) |
当对象注册到反应器时调用的表达式。 args 是一个带括号的函数参数列表,包含拥有的执行上下文、对象的本机句柄和唯一的注册键。 |
BOOST_ASIO_HANDLER_REACTOR_DEREGISTRATION(args) |
当对象从反应器中注销时调用的表达式。 args 是一个带括号的函数参数列表,包含拥有的执行上下文、对象的本机句柄和唯一的注册键。 |
BOOST_ASIO_HANDLER_REACTOR_READ_EVENT |
用于识别反应器读取就绪事件的位掩码常量。 |
BOOST_ASIO_HANDLER_REACTOR_WRITE_EVENT |
用于标识反应器写入准备事件的位掩码常量。 |
BOOST_ASIO_HANDLER_REACTOR_ERROR_EVENT |
用于识别反应器错误准备事件的位掩码常量。 |
BOOST_ASIO_HANDLER_REACTOR_EVENTS(args) |
当注册到反应器的对象准备就绪时调用的表达式。 args 是一个带括号的函数参数列表,包含拥有的执行上下文、唯一的注册键和就绪事件的位掩码。 |
BOOST_ASIO_HANDLER_REACTOR_OPERATION(args) |
当实现作为基于反应器的异步操作的一部分执行系统调用时调用的表达式。 args 是一个带括号的函数参数列表,包含被跟踪的处理程序、操作名称、操作产生的错误代码和(可选)传输的字节数。 |
io_context
构造器允许程序指定一个并发提示。这是对io_context
实现中应用于运行完成处理器的活动线程数的建议。
当后台使用 Windows I/O 完成端口时,此值将传递给 CreateIoCompletionPort
。
当使用基于Reactor的后端时,实现会识别以下特殊的并发提示值:
Value | Description |
---|---|
1 |
该实现假设 io_context 将从单个线程运行,并基于此假设应用多项优化。例如,当一个处理程序从另一个处理程序中发布时,新的处理程序被添加到一个快速线程本地队列(结果是新的处理程序被阻止,直到当前正在执行的处理程序完成)。 |
BOOST_ASIO_CONCURRENCY_HINT_UNSAFE |
这个特殊的并发提示禁用了调度程序和反应器 I/O 中的锁定。 |
BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO |
这个特殊的并发提示禁用反应器 I/O 中的锁定。 |
BOOST_ASIO_CONCURRENCY_HINT_SAFE |
默认值。io_context提供了完整的线程安全性,并且任何线程都可以使用不同的I/O对象。 |
通过定义BOOST_ASIO_CONCURRENCY_HINT_DEFAULT
宏,可以在编译时覆盖默认构造的 io_context
对象使用的并发提示。 例如,在编译期命令行指定
-DBOOST_ASIO_CONCURRENCY_HINT_DEFAULT=1
意味着对程序中所有默认构造的 io_context 对象使用并发提示 1。类似地,可以通过定义 BOOST_ASIO_CONCURRENCY_HINT_1
来覆盖由 1 构造的 io_context 对象使用的并发提示。 例如,传递
-DBOOST_ASIO_CONCURRENCY_HINT_1=BOOST_ASIO_CONCURRENCY_HINT_UNSAFE
给编译期会禁用所有对象的线程安全。
coroutine
类提供无堆栈协程的支持。无堆栈协程使程序能够以最小的开销以同步方式实现异步逻辑,如下例所示:
struct session : boost::asio::coroutine { boost::shared_ptr<tcp::socket> socket_; boost::shared_ptr<std::vector<char> > buffer_; session(boost::shared_ptr<tcp::socket> socket) : socket_(socket), buffer_(new std::vector<char>(1024)) { } void operator()(boost::system::error_code ec = boost::system::error_code(), std::size_t n = 0) { if (!ec) reenter (this) { for (;;) { yield socket_->async_read_some(boost::asio::buffer(*buffer_), *this); yield boost::asio::async_write(*socket_, boost::asio::buffer(*buffer_, n), *this); } } } };
coroutine
类与伪关键字reenter,yield,fork
同时使用。它们是预处理器宏,并使用类似于 Duff's Device 的技术根据 switch 语句实现。 coroutine
类的文档提供了这些伪关键字的完整描述。
spawn()
函数是用于运行堆栈协程的高级包装器。 它基于 Boost.Coroutine 库。 spawn()
函数使程序能够以同步方式实现异步逻辑,如下例所示:
boost::asio::spawn(my_strand, do_echo); // ... void do_echo(boost::asio::yield_context yield) { try { char data[128]; for (;;) { std::size_t length = my_socket.async_read_some( boost::asio::buffer(data), yield); boost::asio::async_write(my_socket, boost::asio::buffer(data, length), yield); } } catch (std::exception& e) { // ... } }
spawn() 的第一个参数可能是一个strand、io_context
或完成处理器。 此参数确定允许协程执行的上下文。 例如,服务器的每个客户端对象可能由多个协程组成; 它们都应该在同一strand上运行,这样就不需要显式同步。
第二个参数是一个带有签名的函数对象,说明指定的代码作为协程的一部分运行:
void coroutine(boost::asio::yield_context yield);
参数 yield 可以传递给异步操作来代替完成处理器,如下所示:
std::size_t length = my_socket.async_read_some(boost::asio::buffer(data),yield);
这将启动异步操作并暂停协程。 异步操作完成后,协程将自动恢复。
其中异步操作的处理程序签名具有以下形式,启动函数返回 result_type。:
void handler(boost::system::error_code ec, result_type result);
在上面的 async_read_some
示例中,这是 size_t。 如果异步操作失败,则将error_code
转换为system_error
异常并抛出。
处理器签名如下形式,启动函数返回void:
void handler(boost::system::error_code ec);
如上所述,错误作为system_error
异常传递回协程。
要从操作中收集 error_code,而不是让它抛出异常,请将输出变量与 yield_context
关联,如下所示:
boost::system::error_code ec; std::size_t length = my_socket.async_read_some( boost::asio::buffer(data), yield[ec]);
注意:如果 spawn()
与 Handler 类型的自定义完成处理程序一起使用,则函数对象签名实际上是:
void coroutine(boost::asio::basic_yield_context<Handler> yield);
通过 awaitable
类模板、use_awaitable
完成标记和co_spawn()
函数提供对 Coroutines TS 的支持。 这些工具允许程序以同步方式实现异步逻辑,结合 co_await 关键字,如以下示例所示:
boost::asio::co_spawn(executor, echo(std::move(socket)), boost::asio::detached); // ... boost::asio::awaitable<void> echo(tcp::socket socket) { try { char data[1024]; for (;;) { std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), boost::asio::use_awaitable); co_await async_write(socket, boost::asio::buffer(data, n), boost::asio::use_awaitable); } } catch (std::exception& e) { std::printf("echo Exception: %s\n", e.what()); } }
co_spawn()
的第一个参数是一个执行器,它确定允许协程执行的上下文。 例如,服务器的每个客户端对象可能由多个协程组成; 它们都应该在同一个strand上运行,这样就不需要显式同步。
第二个参数是一个awaitable<R>
,它是协程入口点函数的返回结果,在上面的例子中是调用echo的结果。 (或者,此参数可以是返回 awaitable<R>
的函数对象。)模板参数 R 是协程生成的返回值的类型。 在上面的例子中,协程返回 void。
第三个参数是一个完成标记,co_spawn()
使用它来生成一个带有签名 void(std::exception_ptr, R)
的完成处理程序。 一旦完成,这个完成处理程序就会被协程的结果调用。 在上面的示例中,我们传递了一个完成标记类型 boost::asio::detached
,它用于显式忽略异步操作的结果。
在这个例子中,协程的主体是在 echo 函数中实现的。 当 use_awaitable
完成令牌传递给异步操作时,此异步操作的启动函数返回一个可与co_await
关键字一起使用的可等待对象:
std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data), boost::asio::use_awaitable);
其中异步操作的处理程序函数签名具有以下形式:
void handler(boost::system::error_code ec, result_type result);
co_await
表达式的结果类型是 result_type。在上面的 async_read_some
示例中,这是 size_t
。 如果异步操作失败,则将error_code
转换为system_error
异常并抛出。
处理程序函数签名为如下形式的:
void handler(boost::system::error_code ec);
co_await
表达式产生一个 void 结果。 如上所述,错误作为 system_error
异常传递回协程。