【问题标题】:boost::asio reasoning behind num_implementations for io_service::strandio_service::strand 的 num_implementations 背后的 boost::asio 推理
【发布时间】:2016-10-27 17:55:51
【问题描述】:

我们已经在生产环境中使用 asio 多年了,最近我们达到了一个临界点,我们的服务器负载刚好足以注意到一个神秘问题。

在我们的架构中,每个独立运行的独立实体都使用个人strand 对象。一些实体可以执行长时间的工作(从文件读取、执行 MySQL 请求等)。显然,这项工作是在用 strand 包裹的处理程序中执行的。一切听起来都很好,应该可以完美地工作,直到我们开始注意到一些不可能的事情,比如计时器在应该的几秒钟后过期,即使线程正在“等待工作”并且工作无缘无故地停止。看起来在一条链中执行的长时间工作对其他不相关的链产生了影响,不是全部,而是大多数。

花费了无数个小时来查明问题。轨迹导致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 共享相同的实现。当这种情况发生时,它是一个死胡同,直到漫长的工作完成。

编辑: 将并行工作减少到工作线程的数量(从10008)并更新了测试运行输出。这样做是因为当两个数字接近时,问题更加明显。

【问题讨论】:

    标签: c++ boost-asio


    【解决方案1】:

    嗯,一个有趣的问题和 +1 给我们一个重现确切问题的小例子。

    “据我所知”,您在 boost 实现中遇到的问题是,默认情况下,它只实例化有限数量的 strand_impl193,正如我在我的 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 完成的工作:489696 真正的 0m5.016s 用户 0m1.620s 系统 0m4.069s MacBook-Pro:asio_test amuralid$ time ./strand_issue 1 产生一个阻塞工作 完成的工作:188480 真正的 0m5.031s 用户 0m0.611s 系统 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 完成的工作:450928 真正的 0m5.017s 用户 0m2.708s 系统 0m3.902s MacBook-Pro:asio_test amuralid$ time ./strand_issue 1 产生一个阻塞工作 完成的工作:458603 真正的 0m5.027s 用户 0m2.611s 系统 0m3.902s

    这两种情况几乎相同!您可能需要根据需要调整宏的值以保持偏差较小。

    【讨论】:

    • “我猜测这样做是为了通过创建大量互斥体来禁止操作系统过载。那会很糟糕。” 为什么?除了小的常量(每个互斥体)内存量之外还有什么开销?
    • @yurikilochek 他们是互斥体。根据定义,除非用于同步,否则它们是无用的。这使得同时等待大量同步原语的集合。 ::WaitForMultipleObjectsEx 可能不介意,但这是一个上下文切换,它不仅仅是几个字节的内存。在 linux 上,没有这样的调用 AFAIK。
    • @Arunmu 无论实现多少次,问题都会持续存在,因为它在设计中。增加数量可能会赢得一些时间,但只是在一定程度上。在实时应用程序中,这永远不会起作用。试试我的例子,work objects 等于线程数,即8 而不是1000。在那种情况下,1024 实现几乎没有帮助 (Work performed:8331)。
    • @GreenScape 我不同意这是一个彻头彻尾的设计问题。如前所述,您必须根据您的要求调整配置宏。您可以尝试在构建中添加-DBOOST_ASIO_ENABLE_SEQUENTIAL_STRAND_ALLOCATION -DBOOST_ASIO_STRAND_IMPLEMENTATIONS=50000 标志并尝试吗?
    • @GreenScape ASIO 基本上只实现了链服务实现池。与您正在执行的操作数量相比,50k 少得多。可能有更好的解决方案,我不知道。我认为值得记录错误/增强票证,或者也许有人已经提出。
    【解决方案2】:

    请注意,如果您不喜欢 Asio 的实现,您可以随时编写自己的 strand,它为每个 strand 实例创建一个单独的实现。对于您的特定平台,这可能比默认算法更好。

    【讨论】:

      【解决方案3】:

      编辑:在最近的 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。但是我怀疑这样的构建没有经过很好的测试。

      【讨论】:

      • 这很有趣@Niall Douglas。查看发行说明,standalone asio 的最后一个版本进入boost 是在 2015 年 4 月。那个版本是 asio 1.10.6 而最新的asio development release 显示 1.10.5 作为最后一个主要版本,所以你是对的,当 Chris 专注于网络库提案时,他们已经分歧了,现在 N4612
      • 不幸的是,strang_impl 的分配策略在独立版本中没有改变。在strand_executor_service 上有一些工作朝着正确的方向发展。我试图将它移植到香草strand_service,但没有运气。当前的设计在很大程度上取决于保证strand_impl 不会被破坏,strand 之后的事件是,如果不重新设计几乎不可能修复。无论如何,我已经写信给邮件列表了。
      • Boost.Asio 和独立的 Asio 现已同步,此答​​案已过时。
      猜你喜欢
      • 1970-01-01
      • 2011-09-17
      • 2021-11-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多