【问题标题】:boost::asio async_receive_from UDP endpoint shared between threads?boost::asio async_receive_from UDP 端点在线程之间共享?
【发布时间】:2022-03-29 07:28:25
【问题描述】:

Boost asio 特别允许多个线程调用 io_service 上的 run() 方法。这似乎是创建多线程 UDP 服务器的好方法。但是,我遇到了一个我正在努力寻找答案的障碍。

看一个典型的 async_receive_from 调用:

m_socket->async_receive_from(
        boost::asio::buffer(m_recv_buffer),
        m_remote_endpoint,
        boost::bind(
            &udp_server::handle_receive,
            this,
            boost::asio::placeholders::error,
            boost::asio::placeholders::bytes_transferred));

远程端点和消息缓冲区不会传递给处理程序,而是处于更高的范围级别(在我的示例中为成员变量)。 UDP 消息到达时处理它的代码如下所示:

void dns_server::handle_receive(const boost::system::error_code &error, std::size_t size)
{
    // process message
    blah(m_recv_buffer, size);

    // send something back
    respond(m_remote_endpoint);
}

如果有多个线程在运行,同步是如何工作的?在线程之间共享单个端点和接收缓冲区意味着 asio 在消息同时到达的情况下在另一个线程中调用处理程序之前等待处理程序在单个线程中完成。这似乎否定了允许多个线程首先调用 run 的意义。

如果我想获得请求的并发服务,看起来我需要将工作包连同端点的副本移交给一个单独的线程,允许处理程序方法立即返回,以便 asio 可以得到on 并将另一条消息并行传递给另一个调用 run() 的线程。

这似乎有点令人讨厌。我在这里错过了什么?

【问题讨论】:

  • 重读你的最后一段,看来你已经掌握了一些东西。但是,交接并不需要真的“讨厌”,正如我希望我的回答显示的那样(post(...) 行就是这样做的)。
  • 我希望服务请求的工作,正如你所说的,是最小的。可悲的是,在我正在处理的情况下,这是一个长期运行的复杂操作,有时需要调用其他服务。我对 Ubuntu 14 进行了一些测试,并且正如猜测的那样,能够证明处理程序永远不会同时调用,无论在 io_service 上调用 run() 的线程数量如何,因此使用提升管理我的线程的方法在后面场景是行不通的。看起来我又回到了显式池方法。谢谢你的回答。
  • 我的答案中的演示确实如此,因为您可以[查看何时进行处理需要 1..3s](),那么该服务肯定会同时完成工作。但是,一般来说,您不希望在 IO 队列上运行 任何 阻塞操作。这正是 the topic of this question。所以是的,要么使阻塞操作异步,要么使用 separate 队列进行阻塞操作(第二个io_service 可能是一个简单的选择)

标签: c++ multithreading boost udp boost-asio


【解决方案1】:

在线程之间共享一个端点和接收缓冲区意味着 asio 等待处理程序在单个线程内完成

如果您的意思是“使用单线程运行服务时”,那么这是正确的。

否则,情况并非如此。相反,当您同时调用单个服务对象(即套接字,而不是 io_service)上的操作时,Asio 只是说行为是“未定义的”。

这似乎否定了允许多个线程首先调用 run 的意义。

除非处理需要相当长的时间。

Timer.5 sample 简介的第一段似乎很好地阐述了您的主题。

会话

要分离特定于请求的数据(缓冲区和端点),您需要一些会话概念。 Asio 中的一种流行机制是绑定 shared_ptrs 或 shared-from-this 会话类(boost bind 支持直接绑定到 boost::shared_ptr 实例)。

为避免对m_socket 成员的并发、非同步访问,您可以添加锁或使用上面链接的Timer.5 示例中记录的strand 方法。

演示

这里是 Daytime.6 异步 UDP 日间服务器,已修改为与许多服务 IO 线程一起使用。

请注意,从逻辑上讲,仍然只有一个 IO 线程(strand),因此我们不会违反套接字类记录的线程安全性。

但是,与官方示例不同,响应可能会乱序排队,具体取决于udp_session::handle_request 中实际处理所花费的时间。

注意

  • udp_session 类用于保存每个请求的缓冲区和远程端点
  • 一个线程池,能够在多个内核上扩展实际处理(而不是 IO)的负载。
#include <ctime>
#include <iostream>
#include <string>
#include <boost/array.hpp>
#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/make_shared.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>

using namespace boost;
using asio::ip::udp;
using system::error_code;

std::string make_daytime_string()
{
    using namespace std; // For time_t, time and ctime;
    time_t now = time(0);
    return ctime(&now);
}

class udp_server; // forward declaration

struct udp_session : enable_shared_from_this<udp_session> {

    udp_session(udp_server* server) : server_(server) {}

    void handle_request(const error_code& error);

    void handle_sent(const error_code& ec, std::size_t) {
        // here response has been sent
        if (ec) {
            std::cout << "Error sending response to " << remote_endpoint_ << ": " << ec.message() << "\n";
        }
    }

    udp::endpoint remote_endpoint_;
    array<char, 100> recv_buffer_;
    std::string message;
    udp_server* server_;
};

class udp_server
{
    typedef shared_ptr<udp_session> shared_session;
  public:
    udp_server(asio::io_service& io_service)
        : socket_(io_service, udp::endpoint(udp::v4(), 1313)), 
          strand_(io_service)
    {
        receive_session();
    }

  private:
    void receive_session()
    {
        // our session to hold the buffer + endpoint
        auto session = make_shared<udp_session>(this);

        socket_.async_receive_from(
                asio::buffer(session->recv_buffer_), 
                session->remote_endpoint_,
                strand_.wrap(
                    bind(&udp_server::handle_receive, this,
                        session, // keep-alive of buffer/endpoint
                        asio::placeholders::error,
                        asio::placeholders::bytes_transferred)));
    }

    void handle_receive(shared_session session, const error_code& ec, std::size_t /*bytes_transferred*/) {
        // now, handle the current session on any available pool thread
        socket_.get_io_service().post(bind(&udp_session::handle_request, session, ec));

        // immediately accept new datagrams
        receive_session();
    }

    void enqueue_response(shared_session const& session) {
        socket_.async_send_to(asio::buffer(session->message), session->remote_endpoint_,
                strand_.wrap(bind(&udp_session::handle_sent, 
                        session, // keep-alive of buffer/endpoint
                        asio::placeholders::error,
                        asio::placeholders::bytes_transferred)));
    }

    udp::socket  socket_;
    asio::strand strand_;

    friend struct udp_session;
};

void udp_session::handle_request(const error_code& error)
{
    if (!error || error == asio::error::message_size)
    {
        message = make_daytime_string(); // let's assume this might be slow

        // let the server coordinate actual IO
        server_->enqueue_response(shared_from_this());
    }
}

int main()
{
    try {
        asio::io_service io_service;
        udp_server server(io_service);

        thread_group group;
        for (unsigned i = 0; i < thread::hardware_concurrency(); ++i)
            group.create_thread(bind(&asio::io_service::run, ref(io_service)));

        group.join_all();
    }
    catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
}

结束的想法

有趣的是,在大多数情况下,您会看到单线程版本的性能也一样,没有理由使设计复杂化。

或者,如果这确实是 CPU 密集型部分,您可以使用专用于 IO 的单线程 io_service 并使用老式工作池对请求进行后台处理。首先,这简化了设计,其次,这可能会提高 IO 任务的吞吐量,因为不再需要协调发布在链上的任务。

【讨论】:

  • 您还可以看到演示 Live On Coliru 以及 8 个并发“远程客户端”。见证响应是如何被乱序接收的。
  • socket_.async_send_to 在链外执行时可能会被同时调用,这违反了线程安全。考虑使用将udp_server::enqueue_response 分派到 I/O 链中的 shim 函数。
【解决方案2】:

由于@sehe 回答的“建议编辑队列”已满,请允许我提交更新。

  • 用线程安全的东西替换了ctime()
  • 更新到最新的增强样式,例如新的boost::bind,以及删除socket_.get_io_service()
  • 去掉using namespace boost,让它更明显
  • 以线程安全的方式调用 async_send_to()(转至 Tanner Sansbury)
#include <iostream>
#include <string>
#include <boost/array.hpp>
#include <boost/bind/bind.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/make_shared.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>

using boost::asio::ip::udp;
using boost::system::error_code;

static std::string make_daytime_string()
{
    return boost::posix_time::to_simple_string(boost::posix_time::second_clock::local_time());
}

class udp_server; // forward declaration

struct udp_session : boost::enable_shared_from_this<udp_session> {

    udp_session(udp_server* server) : server_(server) {}

    void handle_request(const error_code& error);

    void handle_sent(const error_code& ec, std::size_t) {
        // here response has been sent
        if (ec) {
            std::cout << "Error sending response to " << remote_endpoint_ << ": " << ec.message() << "\n";
        }
    }

    udp::endpoint remote_endpoint_;
    boost::array<char, 100> recv_buffer_;
    std::string message;
    udp_server* server_;
};

class udp_server
{
    typedef boost::shared_ptr<udp_session> shared_session;
  public:
    udp_server(boost::asio::io_service& io_service)
        : socket_(io_service, udp::endpoint(udp::v4(), 1313)), 
          strand_(io_service)
    {
        receive_session();
    }

  private:
    void receive_session()
    {
        // our session to hold the buffer + endpoint
        auto session = boost::make_shared<udp_session>(this);

        socket_.async_receive_from(
                boost::asio::buffer(session->recv_buffer_),
                session->remote_endpoint_,
                strand_.wrap(
                    boost::bind(&udp_server::handle_receive, this,
                        session, // keep-alive of buffer/endpoint
                        boost::asio::placeholders::error,
                        boost::asio::placeholders::bytes_transferred)));
    }

    void handle_receive(shared_session session, const error_code& ec, std::size_t /*bytes_transferred*/) {
        // now, handle the current session on any available pool thread
        boost::asio::post(socket_.get_executor(), boost::bind(&udp_session::handle_request, session, ec));

        // immediately accept new datagrams
        receive_session();
    }

    void enqueue_response(shared_session const& session) {
        // async_send_to() is not thread-safe, so use a strand.
        boost::asio::post(socket_.get_executor(),
            strand_.wrap(boost::bind(&udp_server::enqueue_response_strand, this, session)));
    }

    void enqueue_response_strand(shared_session const& session) {
        socket_.async_send_to(boost::asio::buffer(session->message), session->remote_endpoint_,
                strand_.wrap(boost::bind(&udp_session::handle_sent,
                        session, // keep-alive of buffer/endpoint
                        boost::asio::placeholders::error,
                        boost::asio::placeholders::bytes_transferred)));
    }

    udp::socket socket_;
    boost::asio::io_context::strand strand_;

    friend struct udp_session;
};

void udp_session::handle_request(const error_code& error)
{
    if (!error || error == boost::asio::error::message_size)
    {
        message = make_daytime_string(); // let's assume this might be slow
        message += "\n";

        // let the server coordinate actual IO
        server_->enqueue_response(shared_from_this());
    }
}

int main()
{
    try {
        boost::asio::io_service io_service;
        udp_server server(io_service);

        boost::thread_group group;
        for (unsigned i = 0; i < boost::thread::hardware_concurrency(); ++i)
            group.create_thread(bind(&boost::asio::io_service::run, boost::ref(io_service)));

        group.join_all();
    }
    catch (std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多