ускорить сбой сопрограмм сервера при записи данных на клиент - PullRequest
1 голос
/ 10 ноября 2019

Я сделал свой сервер на примере сервера с ускоренным эхо-запуском, просто получает и записывает некоторые данные. Сбой при записи данных клиенту, и, что более странно, сбой только при использовании нескольких ядер.

Вот сервер, он читает 4 байта и записывает обратно «ОК» в течение 1 секунды в качестве тайм-аута:

#include <winsock2.h>
#include <windows.h>

#include <iostream>
using namespace std;

#include <boost/thread/thread.hpp>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
using namespace boost;
using namespace boost::asio;
using namespace boost::asio::ip;

#define SERVER_PORT 1234
#define DATA_LEN_4 4

#define TIMEOUT_LIMIT 1 // second

struct session : public std::enable_shared_from_this<session>
{
    tcp::socket socket_;
    boost::asio::steady_timer timer_;
    boost::asio::strand<boost::asio::io_context::executor_type> strand_;

    explicit session(boost::asio::io_context& io_context, tcp::socket socket)
    : socket_(std::move(socket)),
      timer_(io_context),
      strand_(io_context.get_executor())
    { }

    void go()
    {
        auto self(shared_from_this());
        boost::asio::spawn(strand_, [this, self](boost::asio::yield_context yield)
        {
            try
            {
                timer_.expires_from_now(std::chrono::seconds(TIMEOUT_LIMIT));

                // recv data
                string packet;
                packet.resize(DATA_LEN_4); // alloc memory

                size_t received_len = 0;

                // read data
                {
                    size_t rs;
                    while(received_len < DATA_LEN_4) { // recv 4 bytes
                        boost::system::error_code ec;

                        rs = socket_.async_read_some(
                            boost::asio::buffer((char*)(packet.c_str()+received_len), DATA_LEN_4-received_len), yield[ec]);
                        if(ec==boost::asio::error::eof)
                            break; //connection closed cleanly by peer
                        else if(ec) {
                            throw "read_fail";
                        }
                        received_len += rs;
                    }
                }
                if(received_len < DATA_LEN_4) {
                    throw "recv too short, maybe timeout";
                }
                // write back "OK"
                {
                    boost::system::error_code ecw;
                    boost::asio::async_write(socket_, boost::asio::buffer(string("OK")), yield[ecw]);
                    if(ecw==boost::asio::error::eof)
                        return; //connection closed cleanly by peer
                    else if(ecw)
                        throw "write_fail"; // some other error
                }
            }
            catch (const char* reason) 
            {
                printf("exception reason: %s\n", reason);
                boost::system::error_code ecw;

                /*
                 * Question 1: why this 'async_write' line causes crash?
                 */
                // write the error reason to client
                boost::asio::async_write(socket_, boost::asio::buffer(string(reason)), yield[ecw]);

                socket_.close();
                timer_.cancel();
            }
            catch (...)
            {
                printf("unknown exception\n");
                socket_.close();
                timer_.cancel();
            }
        });

        boost::asio::spawn(strand_, [this, self](boost::asio::yield_context yield)
        {
            while (socket_.is_open())
            {
                boost::system::error_code ignored_ec;
                timer_.async_wait(yield[ignored_ec]);
                if (timer_.expires_from_now() <= std::chrono::seconds(0))
                    socket_.close();
            }
        });
    }
};

int main() {
    boost::asio::io_context io_context;

    boost::asio::spawn(io_context, [&](boost::asio::yield_context yield)
    {
        tcp::acceptor acceptor(io_context,
        tcp::endpoint(tcp::v4(), SERVER_PORT));

        for (;;)
        {
            boost::system::error_code ec;

            tcp::socket socket(io_context);
            acceptor.async_accept(socket, yield[ec]);
            if (!ec) 
                std::make_shared<session>(io_context, std::move(socket))->go();
        }
    });

    /*
     * When run on 1 CPU, it runs fine, no Crash 
     */
    // io_context.run();

    /*
     * Question 2:
     * But when run on multiple CPUs, it Crashes !!!
     * Why?
     */
    auto thread_count = std::thread::hardware_concurrency();
    boost::thread_group tgroup;
    for (auto i = 0; i < thread_count; ++i)
        tgroup.create_thread(boost::bind(&boost::asio::io_context::run, &io_context));
    tgroup.join_all();
}

Обратите внимание: 4-байтовый пакет и 1-секундный тайм-аут просто для иллюстрации проблемы, реальный сервер использует большие пакеты, которые могут вызвать тайм-аут в плохой сетиусловие. Чтобы смоделировать это, клиент записывает 1 байт в секунду, чтобы запустить тайм-аут чтения на сервере.

Клиент:

#include <iostream>
#include <boost/asio.hpp>
using namespace std;

using boost::asio::ip::tcp;

#define SERVER "127.0.0.1"
#define PORT "1234"

int main() {
    boost::asio::io_context io_context;

    unsigned i = 1; 
    while(1) {
        try {
            tcp::socket s(io_context);
            tcp::resolver resolver(io_context);
            boost::asio::connect(s, resolver.resolve(SERVER, PORT));

            // to simulate the bad network condition,
            // write 4 bytes in 4 seconds to trigger the receive timeout on server, which is 1 second
            for(int i=0; i<4; i++) { 
                boost::asio::write(s, boost::asio::buffer(string("A")));
                std::this_thread::sleep_for(std::chrono::seconds(1)); // sleep 1 second
            }

            // read echo
            char x[64] = {0};
            s.read_some(boost::asio::buffer(x, sizeof(x)));
            cout << i++ << ". received: " << x << endl;
        } catch (...) {
            cout << i++ << " exception" << endl;
        }
    }

    return 0;
}

Вопрос 1 :

Почему эта строка вызывает сбой?

boost::asio::async_write(socket_, boost::asio::buffer(string(reason)), yield[ecw]);

Вопрос 2 :

Почему сервер не падает при работе на 1 процессоре: io_context.run();?
И происходит сбой на нескольких процессорах с использованием thread_group?

Моя среда: Win10-64bit, boost-1.71.0-64bit, VisualStudio-2017-Community

1 Ответ

2 голосов
/ 12 ноября 2019

Вопрос 1

ba::async_write(socket_, ba::buffer(string("OK")), yield[ecw]);

Это вызывает неопределенное поведение, поскольку вы передаете временную строку в качестве буфера, но асинхронная операция (по определению) не завершается до возврата вызова async_write.

Следовательно, буфер является устаревшей ссылкой на что-то разрушенное в стеке или на то, что сейчас там живет.

Буфер отправки логически будет частью объекта self, чтобы получить более правильный срок жизни. Или, поскольку вы выполняете сопрограммы и собираетесь в любом случае завершить сеанс, просто используйте write вместо async_write.

Вопрос 2

Это потому, что неопределенное поведение Неопределенное поведение . Может произойти все, что угодно .

Unasked

  • Вместо read_some используйте read с transfer_exactly(DATA_LEN_4) или read_until ссоответствующее условие завершения.

  • Вместо buffer(reserved_string) вы можете dynamic_buffer.

  • Вместо броска магических струн вы можете просто поймать system_error где код указывает, какое возникло условие:

    try {
        timer_.expires_from_now(std::chrono::seconds(TIMEOUT_LIMIT));
    
        // read data
        std::string packet;
        auto received_len = ba::async_read(socket_,
                ba::dynamic_buffer(packet),
                ba::transfer_exactly(DATA_LEN_4), yield);
    
        assert(received_len == DATA_LEN_4); // guaranteed
    
        // write back "OK"
        ba::write(socket_, ba::buffer("OK"s));
    }
    catch (boost::system::system_error const& e) {
        if (e.code() == ba::error::operation_aborted)
            std::cout << "canceled (timeout)" << std::endl;
        else if (e.code() == ba::error::eof)
            std::cout << "eof" << std::endl;
        else throw std::runtime_error(e.code().message());
    }
    
  • Итак, теперь вы можете обернуть это своим общим блоком обработки исключений:

    try {
        // ...
    } catch (std::exception const& e) {
        std::cout << "exception: " << std::quoted(e.what()) << std::endl;
    
        boost::system::error_code ignore;
        ba::async_write(socket_, ba::buffer(std::string(e.what())), yield[ignore]);
    
        socket_.close();
        timer_.cancel();
    }
    

    Но!

    1. кажется весьма сомнительным, что информирование вашего клиента полезно или даже мудро
    2. Если не поймать исключение в coro, это все равно уничтожит экземпляр self, поэтомуВы можете просто позволить ему сбежать

Таймеры

  • Время завершения error_code уже указывает, истек ли таймер или был отменен:

    while (socket_.is_open()) {
        boost::system::error_code ec;
        timer_.async_wait(yield[ec]);
    
        if (ba::error::operation_aborted != ec) // timer was not canceled
            socket_.close();
    }
    
  • Обратите внимание, однако, что обычные пути возврата из сеанса coro НЕ вызывают .cancel() на time_. Это приведет к тому, что сокет останется открытым еще <1 с, пока не истечет таймер. </p>

Исключения

Если вы хотите, чтобы исключения исключались из хранилища (вы можете,и вы должны учитывать, что это происходит), вы должны улучшить циклы потока, обрабатывая исключения: Следует ли перехватывать исключение, генерируемое boost :: asio :: io_service :: run ()?

Предлагаемый код для сервера

Объединение coros и значительное упрощение обработки всех условий:

#include <iostream>
#include <iomanip>

#include <boost/thread/thread.hpp>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/scope_exit.hpp>

using namespace std::literals;
namespace ba = boost::asio;
using ba::ip::tcp;

static constexpr unsigned short SERVER_PORT = 1234;
static constexpr std::size_t    DATA_LEN_4 = 4;
static constexpr auto           TIMEOUT_LIMIT = 1s;

struct session : public std::enable_shared_from_this<session>
{
    tcp::socket socket_;
    ba::steady_timer timer_;
    ba::strand<ba::io_context::executor_type> strand_;

    explicit session(ba::io_context& io_context, tcp::socket socket)
    : socket_(std::move(socket)),
      timer_(io_context),
      strand_(io_context.get_executor())
    { }

    void go() {
        ba::spawn(strand_, [this, self = shared_from_this()](ba::yield_context yield) {

            spawn(yield, [this, self](ba::yield_context yield) {
                timer_.expires_from_now(TIMEOUT_LIMIT);
                while (socket_.is_open()) {
                    boost::system::error_code ec;
                    timer_.async_wait(yield[ec]);
                    if (ba::error::operation_aborted != ec) // timer was not canceled
                        socket_.close();
                }
            });

            try {
                // read data
                std::string packet;
                ba::async_read(socket_,
                        ba::dynamic_buffer(packet),
                        ba::transfer_exactly(DATA_LEN_4), yield);

                // write back "OK"
                ba::write(socket_, ba::buffer("OK"s));
            }
            catch (boost::system::system_error const& e) {
                if (e.code() == ba::error::operation_aborted)
                    std::cout << "canceled (timeout)" << std::endl;
                else if (e.code() == ba::error::eof)
                    std::cout << "eof" << std::endl;
                else // throw std::runtime_error(e.code().message());
                    std::cout << "other: " << e.code().message() << std::endl;
            }

            socket_.close();
            timer_.cancel(); // cancel the other coro so we don't keep the session alive
        });
    }
};

int main() {
    ba::io_context io_context;

    ba::spawn(io_context, [&](ba::yield_context yield) {
        tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), SERVER_PORT));

        for (;;) {
            boost::system::error_code ec;

            tcp::socket socket(io_context);
            acceptor.async_accept(socket, yield[ec]);
            if (!ec) 
                std::make_shared<session>(io_context, std::move(socket))->go();
        }
    });

    boost::thread_group tgroup;
    for (auto i = 0u; i < std::thread::hardware_concurrency(); ++i)
        tgroup.create_thread([&io_context] {
            for (;;) {
                try { io_context.run(); break; } // exited normally
                catch (std::exception const &e) { std::clog << "[eventloop] exception caught " << std::quoted(e.what()) << std::endl; } 
                catch (...)                     { std::clog << "[eventloop] unknown exception caught" << std::endl;                   } 
            }
        });

    tgroup.join_all();
}

С рандомизированным клиентом

Изменение режима сна на случайный, так чтоэто иногда работает и иногда время ожидания:

std::mt19937 prng { std::random_device{}() };
for (int i = 0; i < 4; i++) {
    ba::write(s, ba::buffer(std::string("A")));
    std::this_thread::sleep_for(std::uniform_int_distribution<>(200, 400)(prng) * 1ms);
}

Напечатано на моей системе:

1. received: OK
2. received: OK
3. received: OK
canceled (timeout)
4 exception read_some: End of file
5. received: OK
canceled (timeout)
6 exception read_some: End of file
7. received: OK
8. received: OK

Смотри, Ма, без рук

Еще проще, если исключить специальные сообщения, на самом деле ничего не меняется:

ba::spawn(strand_, [this, self = shared_from_this()](ba::yield_context yield) {
    try {
        ba::steady_timer timer(strand_, TIMEOUT_LIMIT);
        timer.async_wait([this](error_code ec) {
            if (ba::error::operation_aborted != ec) 
                socket_.close();
            });

        std::string packet;
        ba::async_read(socket_,
                ba::dynamic_buffer(packet),
                ba::transfer_exactly(DATA_LEN_4), yield);

        ba::write(socket_, ba::buffer("OK"s));
    } catch(std::exception const& e) {
        std::clog << "error " << std::quoted(e.what()) << std::endl;
    }
});

Обратите внимание, что нам даже не нужен timer_ в качестве члена больше, и егодеструктор автоматически корректно отменяет таймеробласти видимости.

Выходные данные практически не меняются:

1. received: OK
2. received: OK
3. received: OK
error "Operation canceled"
4 exception read_some: End of file
5. received: OK
6. received: OK
7. received: OK
error "Operation canceled"
8 exception read_some: End of file
error "Operation canceled"
9 exception read_some: End of file
...