我们多年来一直在生产中使用 asio ,最近我们已经达到了一个关键点,当我们的服务器加载到足以发现一个神秘的问题时。
在我们的架构中,每个独立运行的独立实体都使用个人strand
对象。一些实体可以执行长时间的工作(从文件读取,执行MySQL请求等)。显然,这项工作是在用钢绞线包裹的处理人员中进行的。所有听起来都很漂亮,应该完美无缺,直到我们开始注意到一些不可能的事情,比如定时器应该在它们应该到期后几秒钟到期,即使线程正在等待工作'并且没有明显理由停止工作。看起来像是在一条链内进行的长时间工作对其他不相关的股线产生了影响,而不是全部,但大多数都是。
花了不少时间来查明问题。该轨道导致了strand
对象的创建方式:strand_service::construct
(here)。
由于某些原因,开发人员决定实施有限数量的strand
。这意味着一些完全不相关的对象将共享一个实现,因此会因此而受到瓶颈。
在独立(非增强) asio 库中,正在使用类似的方法。但是,除了共享实现之外,每个实现现在都是独立的,但可以与其他实现共享mutex
对象(here)。
这是什么一回事?我从未听说过系统中互斥锁数量的限制。或任何与其创建/破坏相关的开销。虽然最后一个问题可以通过回收互斥体而不是破坏它们来轻松解决。
我有一个最简单的测试用例来说明性能下降有多么显着:
#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;
}
使用此命令构建它:
g++ -o asio_strand_test_case -pthread -I/usr/include -std=c++11 asio_strand_test_case.cpp -lboost_system
以通常的方式进行测试:
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
差异是戏剧性的。每个新的非阻塞工作都会创建一个新的strand
对象,直到它与阻塞工作的strand
共享相同的实现。当这种情况发生时,它就是一个死胡同,直到长时间的工作结束。
修改:
将并行工作减少到工作线程数(从1000
到8
)和更新的测试运行输出。这是因为当两个数字都接近时,问题就更明显了。
答案 0 :(得分:3)
嗯,这是一个有趣的问题,+1给我们一个小例子来复制确切的问题。
您遇到的问题&#39;据我所知&#39;使用boost实现的是,它默认情况下只实例化了有限数量的strand_impl
,193
,正如我在我的boost(1.59)版本中看到的那样。
现在,这意味着大量请求将处于争用状态,因为他们将等待锁被另一个处理程序解锁(使用相同的strand_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
现在,有一种方法可以通过设置宏BOOST_ASIO_STRAND_IMPLEMENTATIONS
来更改此缓存实现的数量。
以下是我将其设置为1024后的结果:
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
两种情况几乎相同!您可能希望根据需要调整宏的值,以保持偏差较小。
答案 1 :(得分:1)
编辑:截至最近的Boosts,独立的ASIO和Boost.ASIO现在已经同步。这个答案是为了历史兴趣而保留的。
近年来独立ASIO和Boost.ASIO已经变得相当分散,因为独立的ASIO正逐渐变成参考Networking TS实现的标准化。所有的“动作”都发生在独立的ASIO中,包括主要的错误修复。只对Boost.ASIO进行了非常小的错误修复。他们之间现在有几年的差异。
因此,我建议任何人都可以通过Boost.ASIO找到任何问题,切换到独立的ASIO。转换通常并不难,请查看在config.hpp中切换C ++ 11和Boost的许多宏配置。从历史上看,Boost.ASIO实际上是由独立ASIO的脚本自动生成的,可能是Chris保持这些脚本工作的情况,因此您可以使用所有最新更改重新生成一个品牌闪亮的新Boost.ASIO。我怀疑这样的版本没有经过充分测试。
答案 2 :(得分:0)
请注意,如果您不喜欢Asio的实现,则可以始终编写自己的链,为每个链实例创建一个单独的实现。对于您的特定平台,这可能比默认算法更好。