Boost:: asio рассуждает за num_implementation для io_service:: strand

Мы используем 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;
}

Создайте его, используя следующую команду:

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 работы блокировки. Когда это происходит, это тупик, пока не закончится длительная работа.

Edit: Сокращение параллельной работы до количества рабочих потоков (от 1000 до 8) и обновленного вывода тестового прогона. Это произошло потому, что, когда оба числа близки, проблема более заметна.

Ответ 1

Ну, интересная проблема и +1 для того, чтобы дать нам небольшой пример, воспроизводящий точную проблему.

Проблема, с которой вы сталкиваетесь, как я понимаю, с внедрением 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

Почти одинаково для обоих случаев! Возможно, вы захотите отрегулировать значение макроса в соответствии с вашими потребностями, чтобы уменьшить отклонение.

Ответ 2

Автономные ASIO и Boost.ASIO в последние годы стали довольно отстраненными, поскольку автономный ASIO медленно превращается в стандартную реализацию TS для стандартизации. Все "действия" происходят в автономном ASIO, включая основные исправления ошибок. В Boost.ASIO внесены очень незначительные исправления. К настоящему времени между ними существует разница в несколько лет.

Поэтому я бы предложил всем, кто обнаружил какие-либо проблемы с Boost.ASIO, должен переключиться на автономный ASIO. Преобразование обычно не сложно, посмотрите на многие макроконфигурации для переключения между С++ 11 и Boost в config.hpp. Исторически Boost.ASIO был фактически сгенерирован с помощью script из автономного ASIO, это может быть так, что Крис сохранил эти сценарии, и поэтому вы можете восстановить новый блестящий новый Boost.ASIO с последними изменениями. Я подозреваю, что такая сборка не очень хорошо протестирована.