strands如何保证在boost.asio中正确执行挂起事件

时间:2016-08-23 09:40:25

标签: c++ multithreading boost-asio

考虑使用Boost.asio实现的echo服务器。从连接的客户端读取事件会导致数据块被放置到到达事件队列中。线程池通过这些事件工作 - 对于每个事件,线程获取事件中的数据并将其回送到连接的客户端。

enter image description here

如上图所示,事件队列中可能存在多个来自单个客户端的事件。为了确保按顺序执行和交付给定客户端的这些事件,使用了股数。在这种情况下,来自给定连接客户端的所有事件都将在客户端的链中执行。

我的问题是:股如何保证事件处理的正确顺序?我认为必须存在某种锁定链,但即使这样也不够,所以必须有更多,并且我希望有人可以解释它我们指向一些代码此?

我找到了这个文件: How strands work and why you should use them

它揭示了这个机制,但是说在一个方面" Handler执行顺序不能得到保证"。这是否意味着我们最终可能会永远收到草莓。字段"

此外 - 每当新客户连接时,我们是否必须创建一个新的链,以便每个客户端有一个链?

最后 - 当读取事件到来时,我们如何知道将其添加到哪个链?必须使用连接作为关键字来查找所有股线?

2 个答案:

答案 0 :(得分:6)

strand是一个执行上下文,它在正确的线程上执行临界区内的处理程序。

使用互斥锁实现(或多或少)该关键部分。

有点聪明,因为如果调度程序检测到某个线程已经存在于某个线程中,它会将处理程序附加到一个处理程序队列,以便在关键部分离开之前执行,但是在当前处理程序完成之后

因此在这种情况下,新的处理程序是“排序”发布到当前正在执行的线程。

订购时有一些保证。

strand::post/dispatch(x);
strand::post/dispatch(y);

总是会导致x在y之前发生。

但如果x在执行期间调度处理程序z,则执行顺序为:

x,z,y

请注意,使用strands处理io完成处理程序的惯用方法不是将工作发布到完成处理程序中的一个strand,而是将完成处理程序包装在strand中,并在那里完成工作。

asio包含检测此功能的代码,并且会做正确的事情,确保正确的排序并省略不必要的中间帖子。

e.g:

async_read(sock, mystrand.wrap([](const auto& ec, auto transferred)
{
  // this code happens in the correct strand, in the correct order.
});

答案 1 :(得分:5)

strand为非并发性和处理程序的调用顺序提供了保证; strand无法控制执行和解复用操作的顺序。如果您有以下任一项,请使用strand

  • 多个线程访问非线程安全的共享对象
  • 需要保证顺序排序处理程序

io_service将提供按照启动操作的顺序填充或使用的缓冲区的期望和预期顺序。例如,如果socket永远拥有"草莓字段。"可以阅读,然后给出:

buffer1.resize(11); // buffer is a std::vector managed elsewhere
buffer2.resize(7);  // buffer is a std::vector managed elsewhere
buffer3.resize(8);  // buffer is a std::vector managed elsewhere
socket.async_read_some(boost::asio::buffer(buffer1), handler1);
socket.async_read_some(boost::asio::buffer(buffer2), handler2);
socket.async_read_some(boost::asio::buffer(buffer3), handler3);

操作完成后:

  • handler1被调用,buffer1将包含" Strawberry"
  • handler2已被调用,buffer2将包含"字段"
  • handler3被调用,buffer3将永远包含""

但是,未指定调用完成处理程序的顺序。即使使用strand,此未指定的顺序仍然有效。

操作多路分解

Asio使用Proactor设计模式 [1] 来解复用操作。在大多数平台上,这是根据Reactor实现的。 official documentation提及组件及其职责。请考虑以下示例:

socket.async_read_some(buffer, handler);

调用者是发起者,启动async_read_some异步操作并创建handler完成处理程序。异步操作由StreamSocketService操作处理器执行:

  • 在启动函数中,如果套接字没有其他未完成的异步读取操作且数据可用,则StreamSocketService将从套接字读取并将handler完成处理程序排入io_service
  • 否则,读取操作排队到套接字上,一旦数据在套接字上可用,就通知反应器通知Asio。当io_service运行并且套接字上有数据可用时,反应器将通知Asio。接下来,Asio将从套接字中取出一个未完成的读取操作,执行它,并将handler完成处理程序排入io_service

io_service proactor将使完成处理程序出列,将处理程序解复用到运行io_service的线程,从中执行handler完成处理程序。调用完成处理程序的顺序是未指定的。

多个操作

如果在套接字上启动了多个相同类型的操作,则当前未指定缓冲区的使用顺序或填充顺序。但是,在当前实现中,每个套接字对每种类型的挂起操作使用FIFO队列(例如,用于读取操作的队列;用于写入操作的队列;等等)。 networking-ts草案部分基于Asio,指定:

  

buffers按照发布这些操作的顺序填写。未指定调用这些操作的完成处理程序的顺序。

假设:

socket.async_read_some(buffer1, handler1); // op1
socket.async_read_some(buffer2, handler2); // op2

由于在op1之前启动了op2,所以buffer1保证包含先前在流中收到的数据,而不是buffer2中包含的数据,但handler2可以在handler1之前调用1}}。

组合操作

组合操作由零个或多个中间操作组成。例如,async_read()组合的异步操作由零个或多个中间stream.async_read_some()操作组成。

当前实现使用操作链来创建一个继续,其中启动单个async_read_some()操作,并在其内部完成句柄内,它确定是否启动另一个async_read_some()操作或调用用户的完成处理程序。由于延续,async_read文档要求在组合操作完成之前不会发生其他读取:

  

程序必须确保流不执行任何其他读取操作(例如async_read,流的async_read_some函数或执行读取的任何其他组合操作),直到此操作完成

如果程序违反了这一要求,可能会观察交织数据,因为前面提到了填充缓冲区的顺序。

对于一个具体示例,请考虑启动async_read()操作以从套接字读取26字节数据的情况:

buffer.resize(26); // buffer is a std::vector managed elsewhere
boost::asio::async_read(socket, boost::asio::buffer(buffer), handler);

如果套接字收到"草莓","字段",然后"永远。",那么async_read()操作可能是由一个或多个socket.async_read_some()操作组成。例如,它可以由3个中间操作组成:

  • 第一个async_read_some()操作读取11个字节,包含" Strawberry"从偏移量0开始进入缓冲区。读取26个字节的完成条件尚未满足,因此启动另一个async_read_some()操作以继续操作
  • 第二个async_read_some()操作读取包含"字段"的7个字节。从偏移量11开始进入缓冲区。未满足读取26个字节的完成条件,因此启动另一个async_read_some()操作以继续操作
  • 第三个async_read_some()操作读取包含"永久的8个字节。"从偏移量18开始进入缓冲区。已经满足读取26个字节的完成条件,因此handler被排入io_service

当调用handler完成处理程序时,buffer包含"草莓字段永远。"

strand用于以保证顺序提供处理程序的序列化执行。给出:

  • 一个子对象s
  • 通过f1添加到s链的函数对象s.post(),或s.dispatch()
  • 时添加到s.running_in_this_thread() == false
  • 通过f2添加到s链的函数对象s.post(),或s.dispatch()
  • 时添加到s.running_in_this_thread() == false

然后,strand提供了排序和非并发的保证,这样就不会同时调用f1f2。此外,如果在添加f1之前添加了f2,则会在f1之前调用f2

使用:

auto wrapped_handler1 = strand.wrap(handler1);
auto wrapped_handler2 = strand.wrap(handler2);
socket.async_read_some(buffer1, wrapped_handler1); // op1
socket.async_read_some(buffer2, wrapped_handler2); // op2

由于在op1之前启动了op2,所以buffer1保证包含先前在流中收到的数据,而不是buffer2中包含的数据,但订单其中将调用wrapped_handler1wrapped_handler2的情况未指定。 strand保证:

  • handler1handler2不会同时调用
  • 如果在wrapped_handler1之前调用wrapped_handler2,则会在handler1之前调用handler2
  • 如果在wrapped_handler2之前调用wrapped_handler1,则会在handler2之前调用handler1

与组合操作实现类似,strand实现使用操作链来创建延续。 strand管理在FIFO队列中发布给它的所有处理程序。当队列为空并且将处理程序发布到strand时,strand会将内部句柄发布到io_service。在内部处理程序中,处理程序将从strand的FIFO队列中出列,执行,然后如果队列不为空,则内部处理程序将自身发送回io_service

考虑阅读this答案,了解组合操作如何使用asio_handler_invoke()在完成处理程序的同一上下文(即strand)内包装中间处理程序。可以在此question的评论中找到实施细节。

1。 [POSA2] D. Schmidt等,Pattern Oriented Software Architecture,Volume 2. Wiley,2000。