
第三天,提供了新版本的SObjectizer:5.6.0 。 它的主要特征是拒绝与以前的稳定分支5.5兼容,该分支在过去四年半的时间里稳步发展。
SObjectizer-5的基本操作原理保持不变。 通信,代理,合作和调度员仍在我们身边。 但是有些事情发生了严重变化,有些事情通常被扔掉了。 因此,仅采用SO-5.6.0并重新编译代码将失败。 需要重写某些内容。 某些内容可能必须重新设计。
我们为什么要照顾兼容性好几年,然后才决定收拾一切并破坏一切? 什么最彻底地破坏了?
我将在本文中尝试讨论这一点。
你为什么要破坏一些东西?
就这么简单。
SObjectizer-5.5在其开发过程中吸收了许多原本未计划的不同事物,因此,它在内部形成了太多的拐杖和道具。 对于每个新版本,向SO-5.5添加新的东西变得越来越难。 最后,问题是“为什么我们需要所有这些?” 找不到合适的答案。
因此,第一个原因是SObjectizer内脏的重新复杂化。
第二个原因是我们愚蠢地专注于旧的C ++编译器。 5.5分支始于2014年,当时,如果没有记错的话,我们拥有gcc-4.8和MSVS2013。 在这个级别上,我们仍然继续保持对C ++标准支持级别的要求。
最初,我们对此有“自私的兴趣”。 另外,一段时间以来,我们将对C ++标准支持质量的低要求视为我们的“竞争优势”。
但是随着时间的流逝,“自私的利益”结束了。 这种“竞争优势”带来的一些好处是不可见的。 也许他们会的,如果我们完全使用C ++ 98,那么我们会对血腥的企业感兴趣。 但是,原则上,像我们这样的血腥企业并不感兴趣。 因此,决定停止自我限制,取一些新鲜事物。 因此,我们选择了目前最稳定的版本:C ++ 17。
显然,并不是所有人都喜欢这种解决方案,毕竟,对于许多C ++ 17而言,这已经是无法实现的领先优势,而且仍然非常非常遥远。
尽管如此,我们还是决定了这种风险。 同样,普及SObjectizer的过程并没有很快,因此,当SObjectizer的需求或多或少变得越来越广泛时,C ++ 17将不再是“前沿”。 相反,它将像现在在C ++ 11中一样被对待。
通常,我们决定不再使用C ++ 11的子集继续构建拐杖,而是决定使用C ++ 17认真地重新构建SObjectizer的内部结构。 为SObjectizer在未来四五年内可以逐步发展奠定基础。
在SObjectizer-5.6中发生了什么严重变化?
现在,让我们简要介绍一些最引人注目的变化。
代理合作不再具有字符串名称
问题
从一开始,SObjectizer-5要求每个合作都有其自己的唯一字符串名称。 此功能是从先前的第四个SObjectizer的第五个SObjectizer继承而来的。
因此,SObjectizer需要存储已注册合作的名称。 在注册时检查其唯一性。 在注销等期间按名称搜索合作等。
从第一个版本开始,在SObjectzer-5中使用了一种简单的方案:受互斥量保护的注册合作的单个字典。 注册合作伙伴时,将捕获互斥体,合作伙伴名称的唯一性,父代的存在等。 检查后,修改字典,然后释放互斥锁。 这意味着,如果同时一次开始注册/注销多个合作,则在某些时候它们将暂停并等待,直到其中一项操作完成了合作词典的工作。 因此,合作业务无法很好地扩展。
这就是我要摆脱的问题,以便以合作注册的速度改善情况。
解决方案
考虑了解决该问题的两种主要方法。
首先,存储字符串名称,但是更改字典的存储方式,以便可以扩展合作注册操作。 例如,字典分片,即 将其分为几部分,每部分都将受到其互斥量的保护。
其次,完全拒绝字符串名称,并使用SObjectizer分配的某些标识符。
结果,我们选择了第二种方法,完全放弃了合作社的命名。 现在在SObjectizer中有诸如coop_handle之类的东西,即 向用户隐藏其内容的句柄,但可以将其与std::weak_ptr
进行比较。
注册协作时,SObjectizer返回coop_handle
:
auto coop = env.make_coop(); ...
此句柄应用于注销合作:
auto coop = env.make_coop(); ...
同样,在建立父子关系时应使用此句柄:
SObjectizer Environment中用于合作的存储库的结构也发生了巨大变化。 如果在包含5.5版之前的版本中,它是一本通用词典,那么现在每个合作社都是一个指向儿童合作社链接的存储库。 即 合作社形成一棵树,该树的根在对用户隐藏的特殊根合作社中。
这种结构使得更好地缩放register_coop
和deregister_coop
成为可能:并行操作的相互阻塞只有在它们都属于同一个父母合作的情况下才会发生。 为了清楚起见,这是启动一个特殊的基准测试的结果,该基准通过我在旧笔记本电脑上与Ubuntu 16.04和GCC-7.3的配合来测量操作的性能:
_test.bench.so_5.parallel_parent_child -r 4 -l 7 -s 5 Configuration: roots: 4, levels: 7, level-size: 5 parallel_parent_child: 15.69s 488280 488280 488280 488280 Total: 1953120
即 5.6.0版在大约15.5秒内处理了将近200万次合作。
这是5.5.24.4版本,是当前分支5.5的最后一个版本:
_test.bench.so_5.parallel_parent_child -r 4 -l 7 -s 5 Configuration: roots: 4, levels: 7, level-size: 5 parallel_parent_child: 46.856s 488280 488280 488280 488280 Total: 1953120
相同的情况,但结果差了三倍。
只剩下一种调度员
调度程序是SObjectizer的基石之一。 调度员确定代理将在何处以及如何处理其消息。 因此,如果没有调度程序的想法,可能就不会有SObjectizer。
但是,调度员本身已经发展,演变和预先发展,以至于我们为SObjectizer-5.5创建新的调度员甚至都不困难。 但是很麻烦。 但是,让我们按顺序进行。
最初,只能在SObjectizer的开头创建应用程序所需的所有调度程序:
so_5::launch( []( so_5::environment_t & env ) { },
我没有在开始之前创建必要的调度程序-一切都是我的错,您无法进行任何更改。
很明显,这很不方便,并且随着SObjectizer的使用场景的扩展,有必要解决此问题。 因此, add_dispatcher_if_not_exists
了add_dispatcher_if_not_exists
方法,该方法检查是否存在调度程序,如果没有调度程序,则允许创建一个新实例:
so_5::launch( []( so_5::environment_t & env ) { ...
这样的调度员被称为公众。 公共调度员具有唯一的名称。 并使用这些名称,将代理商绑定到调度员:
so_5::launch( []( so_5::environment_t & env ) { ...
但是公共调度员具有一个不愉快的特征。 在将它们添加到SObjectizer Environment之后,他们立即开始工作,并继续工作直到SObjectizer Environment完成其工作。
再一次,随着时间的流逝,它开始干扰。 有必要确保可以根据需要添加调度程序,并自动删除不需要的调度程序。
因此有“私人”调度员。 这些调度员没有名字,只要有提及他们就活着。 启动SObjectizer Environment之后,可以随时创建私有调度程序,它们会被自动销毁。
总的来说,私人调度员是调度员发展过程中非常成功的纽带,但与公共调度员的合作与之不同:
so_5::launch( []( so_5::environment_t & env ) { ...
甚至更多的私人和公共调度员在实现上有所不同。 因此,为了不复制代码并分别编写相同类型的公共和私人调度程序,我不得不使用带有模板和继承的相当复杂的构造。
结果,我厌倦了所有这些变化,在SObjectizer-5.6中只剩下一种调度程序。 实际上,这类似于私人调度员。 但只是没有明确提及“私人”一词。 因此,现在上面显示的片段将写为:
so_5::launch( []( so_5::environment_t & env ) { ...
只有自由功能send,send_delayed和send_periodic
用于向SObjectizer发送消息的API的开发可能是最令人震惊的示例,说明了随着我们可以使用的编译器对C ++ 11的支持得到改善,SObjectizer发生了变化。
首先,消息是这样发送的:
mbox->deliver_message(new my_message(...));
或者,如果您遵循“最佳犬种推荐”(c):
std::unique_ptr<my_message> msg(new my_message(...)); mbox->deliver_message(std::move(msg));
但是,随后我们可以使用支持可变参数模板和发送功能的编译器。 可以这样写:
send<my_message>(target, ...);
的确,整个家庭从一个简单的send
(包括send_to_agent
, send_delayed_to_agent
等)中send_to_agent
起来需要花费更多时间。 然后,使这个族缩小到熟悉的send
, send_delayed
和send_periodic
。
但是,尽管事实上发送函数家族已经形成很久了,并且已经成为推荐的发送消息方法deliver_message
多年了,但是用户仍然可以使用诸如deliver_message
, schedule_timer
和single_timer
类的旧方法。
但是在版本5.6.0中,只有免费的send
, send_delayed
和send_periodic
函数保存在公共SObjectizer API中。 其他所有内容全部删除或转移到内部SObjectizer命名空间。
因此,在SObjectizer-5.6中,如果我们从一开始就拥有支持常规C ++ 11的编译器,则发送消息的接口最终将变成原来的接口。 好吧,除此之外,如果我们有使用这种非常普通的C ++ 11的经验。
在以前版本的SObjectizer中使用send_delayed
和send_periodic
,又发生了另一件事。
要使用计时器,您必须有权访问SObjectizer Environment。 代理内部有一个指向SObjectizer Environment的链接。 在mchain内部有这样的链接。 但是在mbox内部,她不在那儿。 因此,如果将待处理消息发送到代理或mchain,则send_delayed
调用如下所示:
send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...);
对于mbox,我们必须从其他地方获取指向SObjectizer Environment的链接:
send_delayed<my_message>(this->so_environment(), target_mbox, pause, ...);
send_delayed
和send_periodic
此功能是次要的分裂。 没有那么多的干扰,但是很烦人。 所有这些都是因为最初我们并未开始将指向SObjectizer Environment的链接存储在mbox-ahs中。
违反与先前版本的兼容性是摆脱这种分裂的一个很好的理由。
现在,您可以从mbox中找到为其创建的SObjectizer Environment。 这样就可以对任何类型的计时器消息接收者使用单个send_delayed
和send_periodic
:
send_delayed<my_message>(target_agent, pause, ...); send_delayed<my_message>(target_mchain, pause, ...); send_delayed<my_message>(target_mbox, pause, ...);
从字面上看,“有点琐事,但很好。”
不再有临时代理
俗话说:“每一次事故都有名字,中间名和姓氏。” 对于临时代理,这是我的名字,中间名和姓氏:(
关键是这个。 当我们开始在公共场合谈论SObjectizer-5时,我们听到了很多关于SObjectizer示例代码的冗长性的指责。 从个人角度来看,这种冗长的词在我看来是一个严重的问题,我需要认真对待。
详细程度的一种来源是需要代理从特殊的基本类型agent_t
继承。 从这看来,没有逃脱的可能。 还是不行
因此,有一些临时代理商,即 代理,对于确定不必编写单独的类的决定,仅以lambda函数形式设置对消息的响应就足够了。 例如,关于临时代理的经典乒乓示例可以这样编写:
auto pinger = coop->define_agent(); auto ponger = coop->define_agent(); pinger .on_start( [ponger]{ so_5::send< msg_ping >( ponger ); } ) .event< msg_pong >( pinger, [ponger]{ so_5::send< msg_ping >( ponger ); } ); ponger .event< msg_ping >( ponger, [pinger]{ so_5::send< msg_pong >( pinger ); } );
即 没有自己的课程。 我们只在合作上调用define_agent()
并获得某种代理对象,您可以订阅传入的消息。
因此,在SObjectizer-5中,常规代理和临时代理是分离的。
它没有带来任何明显的奖金,只是伴随这种离职的额外劳动力成本。 随着时间的流逝,很明显,特工就像没有把手的手提箱:难以携带,可惜离开。 但是在使用SObjectizer-5.6时,决定退出。
同时,还吸取了另一个教训,也许更重要了:在互联网上对该工具进行的任何公开讨论中,都会有很多人对这种工具的种类,为何需要它,为什么要使用该工具不感兴趣。 表达他们的强烈意见对他们来说很重要。 除此之外,在Internet的俄语部分中,将工具的愚蠢和未受教育的程度以及不需要多少工作成果传达给工具的开发人员仍然非常重要。
因此,您在输入内容时应该非常小心。 而且您只能(然后仔细地)听一听,这就是我所说的:“我试图那样在您的乐器上这样做,我不喜欢它在这里得到多少代码。” 即使是这样的愿望也应该非常谨慎地对待:“如果在这里和这里会更容易,我会帮助您发展。”
不幸的是,大约五年前,“好心人”在互联网上所说的“过滤”技能远不如现在。 因此,像SObjectizer中的临时代理这样的特定实验。
SObjectizer-5.6不再支持同步的代理交互
代理之间的同步交互主题非常古老且痛苦。
它始于SObjectizer-4时代。 并在SObjectizer-5中继续。 到目前为止,终于,所谓的 服务要求 。 坦白说,最初它令人恐惧。 但是后来我设法给了他们一个或多或少的体面的表情 。
但是事实证明,这是第一个薄煎饼出来时块状的情况:(
在SObjectizer内部,我必须以一种方式实现常规消息的传递和处理,并以另一种方式实现同步请求的传递和处理。 尤其令人遗憾的是,必须考虑这些功能,包括在实现自己的mbox-s时。
并且,在将信封消息的功能添加到SObjectizer之后,变得需要更频繁,更仔细地查看常规消息和同步请求之间的区别。
通常,在SObjectizer维护/开发过程中出现同步请求时,实在太麻烦了。 如此之多,以至于起初有一种具体的愿望,希望摆脱这些非常同步的请求 。 然后,这种愿望得以实现。
因此,在SObjectizer-5.6中,代理只能通过异步消息再次进行交互。
而且由于有时仍需要诸如同步交互之类的东西,因此已向随附的so5extra项目提交了对这种类型的交互的支持:
即 现在处理同步请求根本不同,因为请求处理程序不会像以前那样从处理程序方法中返回值。 而是使用make_reply
方法。
新的实现是好的,因为请求和响应都像常规异步消息一样在SObjectizer内部发送。 实际上, make_reply
是send
更具体的实现。
而且,重要的是,新的实现使我们能够获得以前无法实现的功能:
- 同步请求(即
request_reply_t<Request, Reply>
对象)现在可以保存和/或转发到其他处理程序。 什么使得可以实现各种负载平衡方案; - 您可以在发起请求的代理的常规mbox中做出对请求的响应。 发起方将以通常的方式处理响应,就像其他任何消息一样;
- 您可以一次将多个请求发送给不同的收件人,然后按接收顺序解析来自他们的响应:
using first_dialog = so_5::extra::sync::request_reply_t<first_request, first_reply>; using second_dialog = so_5::extra::sync::request_reply_t<second_request, second_reply>;
因此,可以说,在SObjectizer中进行同步交互时,发生了以下情况:
- 很长一段时间他出于意识形态原因而离开了;
- 然后添加了它,结果发现有时这种交互很有用;
- 但是经验表明,第一个实现不是很成功。
- 原来的实现完全被丢弃了,因此提出了新的实现。
通常,他们确实是为自己的错误而工作。
结论
本文相当简短地讨论了SObjectizer-5.6.0中的一些更改以及这些更改背后的原因。
在此处可以找到更完整的更改列表。
总之,我想提供尚未尝试过SObjectizer的人,请尝试一下。 并与我们分享您的感受:您喜欢什么,您不喜欢什么,缺少什么。
我们会认真听取所有建设性的意见/建议。 而且,近年来,SObjectizer仅包含某人的需求。 因此,如果您不告诉我们您希望在SObjectizer中拥有什么,那么它将不会出现。 如果你告诉我,那么谁知道...;)
该项目现在在这里生活和发展。 对于那些只使用GitHub的人,这里有一个GitHub镜像 。 这面镜子是全新的,因此您可以忽略缺少星星的情况。
PS。 您可以在此Google组中关注SObjectizer相关新闻。 在那里您可以提出与SObjectizer相关的问题。