
开发一个满足开发人员需求的免费框架是一个特定主题。 如果同时框架生存和发展了相当长的时间,那么将添加具体细节。 今天,我将尝试通过一个示例来尝试扩展此示例,以扩展名为
SObjectizer的 C ++的“角色”框架的功能。
事实是,这个框架已经很旧了,已经发生了数次重大变化。 甚至他目前的化身SObjectizer-5也经历了许多变化,无论是严重的还是不是很大的。 此外,我们对兼容性非常敏感,因此引入破坏兼容性的更改对我们来说是一个很重要的步骤,无法决定它。
现在,我们需要决定如何向下一个版本添加新功能。 在寻找合适的解决方案的过程中,出现了两种选择。 两者看起来都可以实现。 但是它们彼此非常不同。 无论是在实施的复杂性和复杂性方面,还是在其“外观”方面。 即 在每个选项中,开发人员处理的内容将有所不同。 可能甚至根本不同。
现在,作为框架的开发人员,我们必须做出选择以选择其中一种解决方案。 或者必须承认它们都不是令人满意的,因此,需要发明一些其他东西。 在SObjectizer的历史过程中,必须多次做出此类决策。 如果有人对这种框架的开发者感兴趣,那么欢迎您。
原始问题
因此,简要介绍一下原始问题的实质。 从它的存在开始,SObjectizer就具有以下功能:取消计时器消息并不是那么容易。 首先,将在计时器下理解延迟消息。 即 一条消息,不应该立即发送给收件人,而要经过一段时间。 例如,我们的send_delayed的暂停时间为1秒。 这意味着实际上,消息将在send_delayed调用之后由计时器1s发送。
挂起的消息原则上可以取消。 如果消息仍在计时器中,则取消后的消息将不会发送到任何地方。 它会被计时器抛出,仅此而已。 但是,如果计时器已经发送了消息,并且该消息现在已在接收代理的请求队列中,则取消计时器将不起作用。 SObjectizer中没有机制可从应用程序队列中删除消息。
至少有两个因素使问题更加复杂。
首先,SObjectizer支持以1:N模式交付,即 如果将邮件发送到Multi-Consumer mbox,则该邮件将不在一个队列中,而是在多个队列中一次供N个收件人使用。
其次,SObjectizer使用分派器机制,分派器可能非常不同,包括用户为满足特定需求而编写的分派器。 请求队列由调度程序管理。 而且,在调度程序的界面中,没有撤消已经转移到调度程序的应用程序的功能。 但是,即使将此类功能嵌入接口中,也无法在所有情况下都有效地实现它。 更不用说这样的功能会增加开发新调度程序的复杂性这一事实。
通常,客观地讲,如果计时器已经向接收者发送了待处理的消息,那么当前不可能强制SObjectizer不传递该消息实例。
实际上,该问题也与周期性消息(即计时器应以预定时间间隔周期性发送的消息)有关。 但是实际上,取消定期消息比取消未决消息要少得多。 至少在我们的实践中是这样。
现在可以做什么?
因此,这个问题并不是新问题,并且很长一段时间以来,对于如何处理它都有一些建议。
待处理邮件中的唯一ID
最简单的方法是保持计数器。 代理具有一个计数器;发送未决消息时,当前计数器值将在消息中发送。 取消消息后,座席处的计数器将增加。 收到消息后,会将代理中的当前计数器值与消息中的值进行比较。 如果值不匹配,则消息被拒绝:
class demo_agent : public so_5::agent_t { struct delayed_msg final { int id_; ... }; int expected_msg_id_{}; so_5::timer_id_t timer_; void on_some_event() {
这种方法的问题在于,需要通过维护这些计数器来困扰代理开发人员。 而且,如果我们需要将其他人的消息作为延迟消息发送,而其他人则这样做,并且其中没有id_字段,那么我们发现自己陷入了困境。
虽然,另一方面,这是当前最有效的方法。
使用唯一的mbox发送延迟的消息
另一个有效的方法是对延迟的邮件使用唯一的邮箱(mbox)。 在这种情况下,我们为每个待处理邮件创建一个新的mbox,对其进行订阅,然后将待处理邮件发送到此mbox。 当需要取消一条消息时,我们只需删除mbox订阅即可。
class demo_agent : public so_5::agent_t { struct delayed_msg final { ...
此方法已经可以处理其他人的消息,在该消息中没有唯一的标识符。 但这也需要开发人员的劳动和关注。
例如,在以上实施例中,没有针对一个较早的消息已经被更早发送的事实的保护。 以一种很好的方式,在发送新的未决消息之前,您应该始终从on_cancel_event()执行操作,否则代理将对此进行不必要的订阅。
为什么以前没有解决过这个问题?
这里的一切都非常简单:实际上,这似乎不是一个严重的问题。 至少在现实生活中,您不必经常处理它。 通常,未决消息和定期消息根本不会被取消(这就是为什么send_delayed函数不返回timer_id的原因)。 并且当需要取消时,可以使用上述方法之一。 甚至使用其他一些。 例如,创建将处理未决消息的单独代理。 当需要取消待处理的消息时,可以注销这些代理。
因此,在我们面临的其他任务的背景下,简化保证取消的挂起消息的优先级并不是要花我们的资源来解决此问题。
为什么现在这个问题很重要?
这里的一切都一样简单。 一方面,双手终于伸手。
另一方面,当没有使用它经验的新手开始使用SObjectizer时,取消计时器的此功能会使他们感到惊讶。 并不令人惊讶。 如果是这样,那么我想尽量减少了解我们的工具的负面印象。
另外,我们有自己的任务,我们不需要不断取消待处理的消息。 新用户有他们自己的任务,也许所有事情都是相反的。
问题的新陈述
几乎立即,一旦开始考虑“保证计时器取消”的可能性,便想到了可以扩展该任务的想法。 您可以尝试解决调用任何先前发送的消息的问题,这些消息不一定是延迟的和定期的。
有时会需要这个机会。 例如,假设我们有几种两种类型的交互代理:entry_point(接受来自客户端的请求)和处理器(处理请求):

Entry_point代理将请求发送到处理器代理,处理器代理将尽可能多地处理它们并响应entry_point代理。 但是有时,entry_point可能会发现不再需要处理先前发送的请求。 例如,客户端发送了取消命令,或者客户端“下降”,您不再需要处理其请求。 现在,如果请求消息已由处理器代理排队,那么您将无法重新调用它们。 这将是有用的。
因此,精确地执行解决“保证的定时器取消”问题的当前方法是增加对“回叫消息”的支持。 我们以一种特殊的方式发送任何消息,我们手边有一个手柄,然后您就可以使用它来调用该消息。 而且,无论是常规消息还是延迟消息,都没有那么重要。
尝试提出“召回消息”的实现
因此,您需要引入“召回消息”的概念,并在SObjectizer中支持该概念。 因此,要留在5.5分支内。 该线程的第一个版本5.5.0大约在四年前(2014年10月)发布。 从那时起,5.5中就没有重大突破。 已经在SObjectize-5.5上切换或立即启动的项目可以切换到5.5分支中的新版本,而不会出现任何问题。 这次必须保持这种兼容性。
总的来说,一切都很简单:您需要采取行动。
清楚怎么办
在第一种解决问题的方法之后,关于“召回消息”的实现有两点变得清晰。
消息处理之前的原子标志及其验证
首先,很明显,在当前SObjectizer-5.5架构的框架内(甚至可能更全局:在SObjectizer-5本身的原理的框架内),不可能从调度程序请求队列中删除消息,在消息队列中消息等待接收代理对其进行处理。 尝试执行此操作将杀死异类调度程序的整个想法,即使用户也可以根据其任务的具体情况(例如,
此任务)来执行
此任务。 另外,在以1:N模式发送消息的情况下(N很大),在所有队列中保留指向已发送消息实例的指针列表将很昂贵。
这意味着必须与消息一起传输某种原子标记,在从请求队列中删除消息之后,但在将消息发送给接收代理进行处理之前,将需要立即分析这些原子标记。 即 邮件会进入队列,并且不会从那里删除。 但是当轮到该消息时,将检查其标志。 并且如果该标志指示该消息已撤回,则该消息不会被处理。
因此,消息调用本身包括为消息内部的原子标志设置一个特殊值。
Revocable_handle_t <M>对象
其次,到目前为止(?)很明显,要发送可撤消的消息,不应该使用通常的发送消息的方法,而应使用代号为revocable_handle_t的特殊对象。
为了发送可撤消消息,用户必须创建一个revocable_handle_t实例,然后在该实例上调用send方法。 如果需要重新调用该消息,则可以使用revoke方法来完成。 类似于:
struct my_message {...}; ... so_5::revocable_handle_t<my_message> msg;
目前尚无revocable_handle_t实现的明确细节,这并不奇怪,因为 尚未选择召回消息的工作机制。 但是工作原理是,在revocable_handle_t中,智能链接将保存到发送的消息以及该消息的原子标记中。 revoke()方法尝试替换标志值。 如果成功,则从订单队列中提取消息后,将不再对其进行处理。
不会和它成为朋友
不幸的是,在某些方面,召回消息无法正确链接。 仅因为撤回的消息继续保留在已到达的队列中。
message_limits
SObjectizer的
message_limits这样重要的功能旨在防止代理过载。 Message_limits根据队列中消息的数量进行工作。 排队的消息-增加了计数器。 脱节-减少。
因为 当邮件被撤消时,它将保留在队列中,然后message_limits不会影响邮件的响应。 因此,可能发现该队列对类型M的消息数有限制,但是所有消息都已被撤回。 实际上,不会处理其中之一。 但是排队M类型的新消息将不起作用,因为 超出限制。
情况不好。 但是如何摆脱呢? 不清楚。
固定队列mchains
在SObjectizer中,不仅可以将消息发送到mbox,还可以发送到mchain(这是
CSP通道的类似物 )。 mchain的队列可以有固定的大小。 尝试将完整大小的固定链中的新消息放入固定链中,将会导致某种反应。 例如,等待队列中的空间释放。 或推送最旧的消息。
在消息重新调用的情况下,它将保留在mchain队列中。 事实证明,不再需要该消息,但是它占用了mchain队列中的空间。 并阻止将新消息发送到mchain。
与message_limits一样的糟糕情况。 同样,还不清楚如何修复它。
不清楚怎么办
因此,我们在实现召回消息的两个(到目前为止?)选项之间进行了选择。 第一个选项易于实现,不需要更改SObjectizer的内脏。 第二种方法要复杂得多,但是其中的消息接收者甚至不知道他正在处理可撤消的消息。 我们将简要地考虑它们中的每一个。
以revocable_t <M>的形式接收可撤销的消息
第一个解决方案首先是可行的,其次是很实用的,是引入了特殊的包装revocable_t <M>。 当用户通过revocable_handle_t <M>发送类型为M的可撤消消息时,发送的不是消息M,而是特殊包装revocable_t <M>内部的消息M。 并且,因此,用户将不会接收和处理类型为M的消息,而是消息revocable_t <M>。 例如,以这种方式:
class processor : public so_5::agent_t { public: struct request { ... };
revocable_t <M> :: try_handle()方法检查原子标志的值,如果未重新调用消息,则调用传递给它的lambda函数。 如果消息已撤回,则try_handle()不执行任何操作。
这种方法的优缺点
最主要的优点是,此行程很容易实现(至少到目前为止似乎如此)。 实际上,revocable_handle_t <M>和revocable_t <M>只是SObjectizer的一个很小的附加组件。
可能需要对SObjectizer内部进行干预才能使朋友成为revocable_t和mutable_msg。 事实是,在SObjectizer中有不可变消息的概念(它们可以以1:1模式和1:N模式发送)。
可变消息的概念只能以1:1模式发送。 在这种情况下,SObjectizer以特殊方式处理mutable_msg <M>标记,并在运行时执行相应的检查。 在revocable_t <mutable_msg <M >>的情况下,您将需要教导SObjectizer将这种构造视为mutable_msg <M>。
另一个优点是,额外的开销(包括可撤消消息的元数据和原子标记的验证)只会在没有它的情况下会发生。 在不使用召回消息的地方,将完全没有额外的开销。
但是主要的缺点是意识形态。 在这种方法中,使用可撤消消息的事实会同时影响发送方(使用revocable_handle_t <M>)和接收方(使用revocable_t <M>)。 但是收件人只是不需要知道他正在接收召回消息。 此外,作为接收者,您可以编写没有revocable_t <M>的现成第三方代理。
另外,关于例如转发此类消息的可能性仍然存在意识形态问题。 但是,根据最初的估计,这些问题已解决。
接收召回消息作为常规消息
第二种方法是仅在接收方查看类型M的消息,而不了解revocable_handle_t <M>和revocable_t <M>的存在。 即 如果处理器应该收到请求,那么它应该只看到请求,而没有任何其他包装。
实际上,采用这种方法不能没有一些包装,但是它们将被隐藏在SObjectizer中,用户不应看到它们。 从队列中检索应用程序之后,SObjectizer会自行确定这是一条特殊包装的可撤消消息,检查消息的相关性标志,并在消息仍然相关时将其展开。 然后,它将向代理发送一条消息,就像处理常规消息一样。
这种方法的优缺点
这种方法的主要优点是显而易见的-邮件接收者不知道他使用什么邮件。 这使消息发送者可以平静地撤回任何代理的消息,即使是其他开发人员编写的消息。
另一个重要的优点是能够与消息传递跟踪机制集成(
这里将详细描述此机制的作用 )。 即 如果启用了msg_tracing,并且发件人撤回了邮件,则可以在msg_tracing日志中找到有关此邮件的痕迹。 调试时非常方便。
但是主要缺点是实现此方法的复杂性。 在此需要考虑几个因素。
首先,开销。 各种各样的事情。
假设您可以在消息中添加一个特殊标志,以指示该消息是否可撤消。 然后在开始处理每条消息之前检查此标志。 粗略地说,在消息传递机制中添加了另一个if,它将在处理每个(!)消息时起作用。
我确信,在实际应用中,这种损失几乎不会引起注意。 但是综合基准的下降肯定会出现。 此外,基准越抽象,他所做的实际工作越少,他就会下沉的越多。 从营销的角度来看,这是不好的,因为 有很多人根据综合基准对框架得出结论。 他们专门这样做:不了解基准测试是什么,它基本上显示了它在哪个硬件上工作,而是将总数与某些情况下在另一种情况下在另一种硬件上的专用工具的性能进行比较。等
通常,由于我们正在创建一个通用框架,事实证明,该框架是由抽象基准中的抽象数字来判断的,因此,我们不想因为
所有功能的增加而浪费了
所有消息传递机制的5%的性能。并不时针对所有用户。
因此,您需要确保在将消息发送给收件人时,SObjectizer理解,提取消息时,需要以特殊方式进行处理。 原则上,当消息传递给代理时,SObjectizer随消息一起存储指向在处理消息时将要使用的函数的指针。 现在需要以不同的方式处理异步消息和同步请求。 实际上,这是发送给代理的消息请求的样子:
struct execution_demand_t {
其中demand_handler_pfn_t是常规函数指针:
typedef void (*demand_handler_pfn_t)( current_thread_id_t, execution_demand_t & );
同样的机制也可以用来专门处理撤回的消息。 即 当mbox向代理发送消息时,代理知道向其发送的是异步消息还是同步请求。 类似地,可以以特殊方式为代理提供异步回调消息。 代理将与消息一起保存指向该函数的指针,该函数知道如何处理已撤销的消息。
一切似乎都很好,但是有两个很大的“ buts” ... :(
首先,现有的mbox接口(即
abstract_message_mbox_t类)没有用于发送撤回消息的方法。 因此,该接口需要扩展。 因此,与SObjectizer-5.5中的abstract_message_box_t关联的其他人的mbox实现不会中断(特别是mbox系列是在
so_5_extra中实现的,我只是不想破坏它们)。
其次,消息不仅可以发送到隐藏了代理的mbox-s,还可以发送到mchain-s。
与CSP渠道相对应的是哪些。 直到现在,这些应用程序还没有放置任何指向功能的指针。 在应用程序队列mchain的每个元素中引入一个附加的指针……当然可以,但是它看起来是一个相当昂贵的解决方案。 此外,到目前为止,mchain实现本身还没有提供以下情况:提取的消息需要检查并且可能被丢弃。
如果您尝试总结上述所有问题,则此方法的主要问题在于实现起来并不容易,因此对于不使用召回消息的情况而言,这种方法很便宜。
但是,如何保证取消待处理消息呢?
恐怕最初的问题已经在技术细节中迷失了。假设有可撤销的消息,如何取消待处理/定期消息?正如他们所说,在这里可以选择。例如,处理未决/定期消息可能是revocable_handle_t <M>功能的一部分: revocable_handle_t<my_mesage> msg; msg.send_delayed(target, 15s, ...); ... msg.revoke();
或者,您可以在revocable_handle_t <M>的顶部添加一个附加的辅助类cancelable_timer_t <M>,该类将提供send_delayed / send_periodic方法。白点:同步请求
SObjectizer-5不仅支持程序中实体之间的异步交互(通过向mbox和mchain发送消息),还支持通过request_value / request_future进行的同步交互。这种同步交互不仅适用于代理。即
您不仅可以通过代理mbox向代理发送同步请求。对于mchain,您还可以向另一个工作线程发出同步请求,例如,在另一个工作线程上,对mchain调用了receive()或select()。因此,仍然不清楚是否应允许将同步请求与可撤销消息一起使用。一方面,也许这是有道理的。例如,它看起来可能像这样: revocable_handle_t<my_request> msg; auto f = msg.request_future<my_reply>(target, ...); ... if(some_condition) msg.revoke(); ... f.get();
另一方面,仍然有很多无法理解的消息和召回消息,因此同步交互的问题已推迟到更好的时候。选择,但要小心。但是选择
这样就对问题有了了解。有两种解决方法。目前看来可行。但是它们在提供给用户的便利程度上有很大的不同,甚至更强烈地在实现成本上有差异。您必须在这两个选项之间进行选择。或提出其他建议。选择的困难是什么?困难在于SObjectizer是一个免费框架。他没有直接给我们带来金钱。正如他们所说,我们这样做是为了我们自己。因此,纯粹从经济偏好出发,实施一个更简单,更快捷的选择会更有利可图。但是,另一方面,并不是所有的东西都用金钱来衡量,从长远来看,一个功能良好的工具通常会相互链接,其功能要好于以某种方式粘贴在一起的补丁组成的拼凑而成的补丁。当我们随后进行开发并为其添加新功能时,质量将由用户和我们自己进行评估。因此,实际上是在短期收益和长期前景之间做出选择。的确,在现代世界中,具有长期前景的C ++工具有些模糊。这使得选择更加困难。在这种情况下,您必须选择。注意事项但是选择。结论
在本文中,我们试图展示一些在我们的框架中设计和实现新功能的过程。我们定期进行这样的过程。以前常常是因为 在2014-2016年间,SObjectizer的发展更为积极。现在,新版本的发布速度有所降低。这是客观的,包括因为在不破坏任何内容的情况下添加新功能,每个新版本都变得更加困难。我希望对我们幕后看很有趣。感谢您的关注!