C++ io_服务::strand的num_实现背后的boost::asio推理
多年来,我们一直在生产中使用asio,最近,当我们的服务器加载到足以注意到一个神秘问题时,我们达到了一个临界点 在我们的体系结构中,每个独立运行的独立实体都使用一个个人C++ io_服务::strand的num_实现背后的boost::asio推理,c++,boost-asio,C++,Boost Asio,多年来,我们一直在生产中使用asio,最近,当我们的服务器加载到足以注意到一个神秘问题时,我们达到了一个临界点 在我们的体系结构中,每个独立运行的独立实体都使用一个个人串对象。一些实体可以执行长时间的工作(读取文件、执行MySQL请求等)。显然,工作是在用钢绞线包裹的处理程序中执行的。所有这些听起来都很好,很漂亮,应该可以完美地工作,直到我们开始注意到一个不可能的事情,比如计时器在应该的几秒钟后过期,即使线程正在“等待工作”,并且工作无缘无故地停止。看起来,在一条链中执行的长时间工作对其他不相关
串对象。一些实体可以执行长时间的工作(读取文件、执行MySQL请求等)。显然,工作是在用钢绞线包裹的处理程序中执行的。所有这些听起来都很好,很漂亮,应该可以完美地工作,直到我们开始注意到一个不可能的事情,比如计时器在应该的几秒钟后过期,即使线程正在“等待工作”,并且工作无缘无故地停止。看起来,在一条链中执行的长时间工作对其他不相关的链产生了影响,不是所有的,而是大多数
花了无数个小时来查明这个问题。这条轨迹引导了创建strand
对象的方式:strand\u服务::construct
()
出于某种原因,开发人员决定使用数量有限的strand
实现。这意味着一些完全不相关的对象将共享一个实现,因此将因此受到限制
在独立(非boost)asio库中,使用了类似的方法。但不是共享实现,每个实现现在是独立的,但可以与其他实现共享一个互斥对象()
这是怎么回事?我从来没有听说过对系统中互斥体数量的限制。或与其创建/销毁相关的任何开销。尽管最后一个问题可以通过回收互斥体而不是销毁互斥体来轻松解决
我有一个最简单的测试用例来说明性能下降有多严重:
#include <boost/asio.hpp>
#include <atomic>
#include <functional>
#include <iostream>
#include <thread>
std::atomic<bool> running{true};
std::atomic<int> counter{0};
struct Work
{
Work(boost::asio::io_service & io_service)
: _strand(io_service)
{ }
static void start_the_work(boost::asio::io_service & io_service)
{
std::shared_ptr<Work> _this(new Work(io_service));
_this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this)));
}
static void do_the_work(std::shared_ptr<Work> _this)
{
counter.fetch_add(1, std::memory_order_relaxed);
if (running.load(std::memory_order_relaxed)) {
start_the_work(_this->_strand.get_io_service());
}
}
boost::asio::strand _strand;
};
struct BlockingWork
{
BlockingWork(boost::asio::io_service & io_service)
: _strand(io_service)
{ }
static void start_the_work(boost::asio::io_service & io_service)
{
std::shared_ptr<BlockingWork> _this(new BlockingWork(io_service));
_this->_strand.get_io_service().post(_this->_strand.wrap(std::bind(do_the_work, _this)));
}
static void do_the_work(std::shared_ptr<BlockingWork> _this)
{
sleep(5);
}
boost::asio::strand _strand;
};
int main(int argc, char ** argv)
{
boost::asio::io_service io_service;
std::unique_ptr<boost::asio::io_service::work> work{new boost::asio::io_service::work(io_service)};
for (std::size_t i = 0; i < 8; ++i) {
Work::start_the_work(io_service);
}
std::vector<std::thread> workers;
for (std::size_t i = 0; i < 8; ++i) {
workers.push_back(std::thread([&io_service] {
io_service.run();
}));
}
if (argc > 1) {
std::cout << "Spawning a blocking work" << std::endl;
workers.push_back(std::thread([&io_service] {
io_service.run();
}));
BlockingWork::start_the_work(io_service);
}
sleep(5);
running = false;
work.reset();
for (auto && worker : workers) {
worker.join();
}
std::cout << "Work performed:" << counter.load() << std::endl;
return 0;
}
以常规方式进行试运行:
time ./asio_strand_test_case
Work performed:6905372
real 0m5.027s
user 0m24.688s
sys 0m12.796s
长阻塞工作的试运行:
time ./asio_strand_test_case 1
Spawning a blocking work
Work performed:770
real 0m5.031s
user 0m0.044s
sys 0m0.004s
差别是巨大的。每个新的非阻塞工作创建一个新的串
对象,直到它与阻塞工作的串
共享相同的实现。当这种情况发生时,这是一条死胡同,直到漫长的工作结束
编辑:
将并行工作减少到工作线程数(从1000
减少到8
),并更新测试运行输出。之所以这样做,是因为当两个数字接近时,问题更为明显。好吧,这是一个有趣的问题,+1为我们提供了一个复制确切问题的小示例
“据我所知”,boost实现存在的问题是,默认情况下,它只实例化了有限数量的strand\u impl
,193
,正如我在boost版本(1.59)中看到的那样
现在,这意味着大量请求将处于争用状态,因为它们将等待另一个处理程序解锁锁(使用相同的strand\u impl
)
我认为这样做是不允许通过创建大量的互斥来重载操作系统的。那太糟糕了。当前的实现允许重用锁(并且以可配置的方式,我们将在下面看到)
在我的设置中:
MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -O2 -o strand_issue strand_issue.cc -lboost_system -pthread
MacBook-Pro:asio_test amuralid$ time ./strand_issue
Work performed:489696
real 0m5.016s
user 0m1.620s
sys 0m4.069s
MacBook-Pro:asio_test amuralid$ time ./strand_issue 1
Spawning a blocking work
Work performed:188480
real 0m5.031s
user 0m0.611s
sys 0m1.495s
两种情况几乎相同!您可能需要根据需要调整宏的值,以保持较小的偏差。编辑:自最近的升级以来,独立ASIO和Boost.ASIO现在处于同步状态。这个答案是为了历史利益而保留的
独立ASIO和Boost.ASIO近年来变得相当独立,因为独立ASIO慢慢演变为标准化的参考网络TS实现。所有的“动作”都发生在独立的ASIO中,包括主要的bug修复。Boost.ASIO只做了很小的错误修复。到现在为止,他们之间的差距已经有好几年了
因此,我建议任何发现Boost.ASIO有任何问题的人都应该切换到独立的ASIO。转换通常不难,看看C++中的11个宏配置和在CONT.HPP中的Boost之间的切换。从历史上看,Boost.ASIO实际上是由独立ASIO的脚本自动生成的,可能是Chris一直让这些脚本正常工作的情况,因此您可以使用所有最新的更改重新生成一个全新的Boost.ASIO。但是,我怀疑这样的构建没有经过很好的测试。请注意,如果您不喜欢Asio的实现,您可以编写自己的串,为每个串实例创建单独的实现。这可能比默认算法更适合您的特定平台。“我想这样做的原因是不允许通过创建大量的互斥来重载操作系统。这很糟糕。”为什么?除了较小的常量(每个互斥体)内存之外,还有什么开销?@yurikilochek它们是互斥体。根据定义,除非用于同步,否则它们是无用的。这使得大量的同步原语被同时等待<代码>::WaitForMultipleObjectsEx
可能不介意,但这是一个上下文切换,而不仅仅是几个字节的内存。在linux上,没有这样的调用AFAIK。@Arunmu无论实现多少,这个问题都会持续存在,因为它在设计中。增加数量可能会赢得一些时间,但只能在一定程度上。在实时应用程序中,这永远不会起作用。请尝试我的示例,使工作对象
等于线程数,即8
而不是1000
。在这种情况下,1024
实现几乎没有帮助(完成的工作:8331
)。@GreenScape我不同意这是一个彻底的设计问题。如前所述,您必须根据需要调整配置宏。您可以尝试在构建中添加-DBOOST\u ASIO\u ENABLE\u SEQUENTIAL\u STRAND\u ALLOCATION-DBOOST\u ASIO\u STRAND\u IMPLEMENTATIONS=50000
标志并重试吗?@GreenScape ASIO是
MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -O2 -o strand_issue strand_issue.cc -lboost_system -pthread
MacBook-Pro:asio_test amuralid$ time ./strand_issue
Work performed:489696
real 0m5.016s
user 0m1.620s
sys 0m4.069s
MacBook-Pro:asio_test amuralid$ time ./strand_issue 1
Spawning a blocking work
Work performed:188480
real 0m5.031s
user 0m0.611s
sys 0m1.495s
MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -DBOOST_ASIO_STRAND_IMPLEMENTATIONS=1024 -o strand_issue strand_issue.cc -lboost_system -pthread
MacBook-Pro:asio_test amuralid$ time ./strand_issue
Work performed:450928
real 0m5.017s
user 0m2.708s
sys 0m3.902s
MacBook-Pro:asio_test amuralid$ time ./strand_issue 1
Spawning a blocking work
Work performed:458603
real 0m5.027s
user 0m2.611s
sys 0m3.902s