对于io_service :: strand的num_implementations背后的boost :: asio推理

Gre*_*ape 15 c++ boost-asio

我们已经在生产中使用asio多年了,最近我们已经达到一个关键点,当我们的服务器加载到足以注意到一个神秘的问题.

在我们的体系结构中,独立运行的每个独立实体都使用个人strand对象.一些实体可以执行长时间的工作(从文件读取,执行MySQL请求等).显然,这项工作是在用钢绞线包裹的处理人员中进行的.所有听起来都很漂亮,应该完美无缺,直到我们开始注意到一些不可能的事情,比如计时器应该在它们应该到期后几秒钟到期,即使线程正在"等待工作"并且工作停止没有明显的原因.看起来像是在一条链内进行的长时间工作对其他不相关的股线产生了影响,而不是全部,但大多数.

花了不少时间来查明问题.轨道导致的方式strand创建对象:strand_service::construct(这里).

出于某种原因,开发人员决定strand实施数量有限.这意味着一些完全不相关的对象将共享一个实现,因此会因此受到瓶颈.

在独立(非增强)asio库中,正在使用类似的方法.但是,不是共享实现,每个实现现在都是独立的,但可以mutex与其他实现共享一个对象(这里).

这是什么一回事呢?我从未听说过系统中互斥锁数量的限制.或任何与其创建/破坏相关的开销.虽然最后一个问题可以通过回收互斥体而不是破坏它们来轻松解决.

我有一个最简单的测试用例来说明性能下降有多么戏剧性:

#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;
}
Run Code Online (Sandbox Code Playgroud)

使用此命令构建它:

g++ -o asio_strand_test_case -pthread -I/usr/include -std=c++11 asio_strand_test_case.cpp -lboost_system
Run Code Online (Sandbox Code Playgroud)

以通常的方式进行测试:

time ./asio_strand_test_case 
Work performed:6905372

real    0m5.027s
user    0m24.688s
sys     0m12.796s
Run Code Online (Sandbox Code Playgroud)

使用长时间阻塞工作进行测试运行:

time ./asio_strand_test_case 1
Spawning a blocking work
Work performed:770

real    0m5.031s
user    0m0.044s
sys     0m0.004s
Run Code Online (Sandbox Code Playgroud)

差异是戏剧性的.会发生什么是每个新的非阻塞工作创建一个新strand对象,直到它与strand阻塞工作共享相同的实现.当这种情况发生时,这是一个死胡同,直到长时间的工作结束.

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

Aru*_*nmu 4

嗯,这是一个有趣的问题,+1 为我们提供了一个重现确切问题的小例子。

正如我在我的 boost 版本(1.59)中看到的那样,您在 boost 实现中遇到的“据我所知”的问题是,默认情况下它仅实例化有限数量的strand_impl193

现在,这意味着大量请求将出现争用,因为它们将等待其他处理程序解锁锁(使用相同的 实例strand_impl)。

我的猜测是,做这样的事情是通过创建大量的互斥锁来禁止操作系统过载。那会很糟糕。当前的实现允许重复使用锁(并且以一种可配置的方式,如下所示)

在我的设置中:

MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -O2 -ostrand_issuestrand_issue.cc -lboost_system -pthread
MacBook-Pro:asio_test amuralid$ 时间 ./strand_issue
已完成工作:489696

真实0m5.016s
用户0m1.620s
系统0m4.069s
MacBook-Pro:asio_test amuralid$ 时间 ./strand_issue 1
产生阻塞性工作
已完成工作:188480

真实0m5.031s
用户0m0.611s
系统0米1.495秒

现在,有一种方法可以通过设置 Macro 来更改缓存实现的数量BOOST_ASIO_STRAND_IMPLEMENTATIONS

下面是我将其设置为 1024 后得到的结果:

MacBook-Pro:asio_test amuralid$ g++ -std=c++14 -DBOOST_ASIO_STRAND_IMPLMENTATIONS=1024 -ostrand_issuestrand_issue.cc -lboost_system -pthread
MacBook-Pro:asio_test amuralid$ 时间 ./strand_issue
已完成工作:450928

真实0m5.017s
用户0m2.708s
系统0m3.902s
MacBook-Pro:asio_test amuralid$ 时间 ./strand_issue 1
产生阻塞性工作
完成的工作:458603

真实0m5.027s
用户0m2.611s
系统0m3.902s

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