考虑使用Boost.asio实现的echo服务器。从连接的客户端读取事件会导致数据块被放置到到达事件队列中。线程池通过这些事件工作 - 对于每个事件,线程获取事件中的数据并将其回送到连接的客户端。
如上图所示,事件队列中可能存在多个来自单个客户端的事件。为了确保按顺序执行和交付给定客户端的这些事件,使用了股数。在这种情况下,来自给定连接客户端的所有事件都将在客户端的链中执行。
我的问题是:股如何保证事件处理的正确顺序?我认为必须存在某种锁定链,但即使这样也不够,所以必须有更多,并且我希望有人可以解释它我们指向一些代码此?
我找到了这个文件: How strands work and why you should use them
它揭示了这个机制,但是说在一个方面" Handler执行顺序不能得到保证"。这是否意味着我们最终可能会永远收到草莓。字段"
此外 - 每当新客户连接时,我们是否必须创建一个新的链,以便每个客户端有一个链?
最后 - 当读取事件到来时,我们如何知道将其添加到哪个链?必须使用连接作为关键字来查找所有股线?
答案 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操作处理器执行:
handler
完成处理程序排入io_service
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提供了排序和非并发的保证,这样就不会同时调用f1
和f2
。此外,如果在添加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_handler1
和wrapped_handler2
的情况未指定。 strand
保证:
handler1
和handler2
不会同时调用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。