Boost.Asio Source Reading: Organization Structure

(This article is based on Boost 1.69)
Boost.Asio code style.Asio divides the declaration and implementation of some of the more complex classes into two header files for readability, and include s the header file of the implementation at the end of the header file of the declaration.The impl folder contains header files for these implementations.Another common keyword is detail, where specific code for different operating systems is placed in the detail folder.
First look at io_context.The main functions of io_context are accepting tasks, handling IO events, and callbacks.
Reading the source code, we found that io_context contains a concrete implementation of io_context, which is win_iocp_io_context on the windows platform, implements the native proactor, while the other platforms are a scheduler.

// file: <boost/asio/io_context.hpp>
...
#if defined(BOOST_ASIO_HAS_IOCP)
typedef class win_iocp_io_context io_context_impl;
class win_iocp_overlapped_ptr;
#else
typedef class scheduler io_context_impl;
#endif
...

Let's take a look at scheduler.A scheduler contains a reactor, which simulates a proactor through a reactor: the user's interface is consistent, but data replication is done in the user state, not in the kernel state.
Off-topic: linux also has an asynchronous io interface, but it is not mature compared to windows iocp.In addition, browsing the source code shows that reactors are implemented differently on different platforms, with linux as epoll_reactor based on epoll and macOS as kqueue_reactor based on kqueue.

// file: <boost/asio/detail/reactor_fwd.hpp>
...
#if defined(BOOST_ASIO_HAS_IOCP) || defined(BOOST_ASIO_WINDOWS_RUNTIME)
typedef class null_reactor reactor;
#elif defined(BOOST_ASIO_HAS_IOCP)
typedef class select_reactor reactor;
#elif defined(BOOST_ASIO_HAS_EPOLL)
typedef class epoll_reactor reactor;
#elif defined(BOOST_ASIO_HAS_KQUEUE)
typedef class kqueue_reactor reactor;
#elif defined(BOOST_ASIO_HAS_DEV_POLL)
typedef class dev_poll_reactor reactor;
#else
typedef class select_reactor reactor;
#endif
...

Schduler schedules tasks.First, the variables that are closely related to scheduler are introduced:

  • scheduler member op_queue_: operation queue
  • Schduler member task_:reactor
  • Thread Private Queue

There are two scheduler-related queues, where the scheduler's operation queue holds general operations and the thread's private queue holds reactor-related operations.These two types of operations need to be handled separately and placed in two queues.Looking at the Asio source, scheduler::run calls scheduler::do_run_once, where the reactor member function run accepts a thread private queue.

// file: <boost/asio/detail/impl/scheduler.ipp>
...
std::size_t scheduler::do_run_one(mutex::scoped_lock& lock,
scheduler::thread_info& this_thread,
const boost::system::error_code& ec)
{
while (!stopped_)
{
if (!op_queue_.empty())
{
// Prepare to execute first handler from queue.
operation* o = op_queue_.front();
op_queue_.pop();
bool more_handlers = (!op_queue_.empty());
if (o == &task_operation_)
{
task_interrupted_ = more_handlers;
if (more_handlers && !one_thread_)
wakeup_event_.unlock_and_signal_one(lock);
else
lock.unlock();
task_cleanup on_exit = { this, &lock, &this_thread };
(void)on_exit;
// Run the task. May throw an exception. Only block if the operation
// queue is empty and we're not polling, otherwise we want to return
// as soon as possible.
task_->run(more_handlers ? 0 : -1, this_thread.private_op_queue);
}
else
{
std::size_t task_result = o->task_result_;
if (more_handlers && !one_thread_)
wake_one_thread_and_unlock(lock);
else
lock.unlock();
// Ensure the count of outstanding work is decremented on block exit.
work_cleanup on_exit = { this, &lock, &this_thread };
(void)on_exit;
// Complete the operation. May throw an exception. Deletes the object.
o->complete(this, ec, task_result);
return 1;
}
}
else
{
wakeup_event_.clear(lock);
wakeup_event_.wait(lock);
}
}
return 0;
}
...

General operations can be passed directly to the scheduler, but reactor is a private member of the scheduler, so how can the user let the reactor complete the related io operations, or how can the user generate an operation for the reactor (cross-platform issues also need to be considered, in the windows platform it is for the iocp-based Proactor operation)?This requires an additional abstraction, specifically, that Asio provides some type of generic interface through services.For example, looking at the Asio source code shows that reactive_socket_service under unix platform and win_iocp_socket_service under Windows platform implement the interface of io operation based on reactor/proactor, including socket creation, asynchronous sending, receiving, etc. Handler is the callback class after io operation is completed.With these services, specific implementations such as ip::tcp::socket, ip::udp::socket, etc. can operate reactor/proactor indirectly by calling a service's uniform interface.

// file: <boost/asio/detail/reactive_descriptor_service.hpp>
template <typename Protocol>
class reactive_socket_service :
public service_base<reactive_socket_service<Protocol> >,public reactive_socket_service_base
{
...
template <typename Handler>
void async_send_to(implementation_type& impl, const null_buffers&,
const endpoint_type&, socket_base::message_flags, Handler& handler)
...
}
// file: <boost/asio/detail/win_iocp_socket_service.hpp>
template <typename Protocol>
class win_iocp_socket_service :public service_base<win_iocp_socket_service<Protocol> >,public win_iocp_socket_service_base
{
...
template <typename Handler>
void async_send_to(implementation_type& impl, const null_buffers&,const endpoint_type&, socket_base::message_flags, Handler& handler)
...
}

Specific implementation of the service "pattern": how the service accesses the internal io_context and registers the service.
The service accesses the internal implementation of io_context.Asio makes other symbol s accessible to private members of io_context and private members of io_context private member scheduler through the friend declaration
Introduction to Registration Services.Asio contains a registered service class service_registry.Looking at the Asio source code, service_registry contains a list of services, the owner of service_registry (where execution_context is the base class of io_context).

// file: <boost/asio/detail/service_registry.hpp>
class service_registry: private noncopyable
{
public:
// Constructor.
BOOST_ASIO_DECL service_registry(execution_context& owner);
...
private:
...
// The owner of this service registry and the services it contains.
execution_context& owner_;
// The first service in the list of contained services.
execution_context::service* first_service_;
};

Specific implementation of registration service.Clearly, the functions that need to be implemented around the service are:
Get service and registration service.Obviously, what users are most concerned about is how to get services easily.When a user acquires a service, the parameter provided is the class of the service (as a template parameter for a function template). Looking at the source code, asio can see that the class is converted into a specific key by typeid or the address of the static data member id of the service class, and the service is registered and searched by key.
Generate services.The service's construction parameter is io_context, which is simple enough to construct on the heap and save the pointer.

// file: <boost/asio/execution_context.hpp>
...
struct key
{
key() : type_info_(0), id_(0) {}
const std::type_info* type_info_;
const execution_context::id* id_;
} key_;
...
// file: <boost/asio/detail/impl/service_registry.hpp>
...
template <typename Service>
void service_registry::init_key_from_id(execution_context::service::key& key,const service_id<Service>& /*id*/)
{
key.type_info_ = &typeid(typeid_wrapper<Service>);
key.id_ = 0;
}
...
// file: <boost/asio/detail/impl/service_registry.ipp>
...
void service_registry::init_key_from_id(execution_context::service::key& key,const execution_context::id& id)
{
key.type_info_ = 0;
key.id_ = &id;
}
...

Why use service "mode"?Obviously, Asio's design is influenced by the proactor/reactor (cross-platform) itself, and additional abstraction is necessary to deal with cross-platform issues.The service mode makes the interface of the Asio library more organized, hierarchical and flexible.Specifically:

  • Ordinary users directly use public member functions of ip::tcp::socket and io_context, and the io_context internal proactor implements hiding from users.
  • Developers responsible for ip::tcp::socket, ip::udp::socket, etc. interact with proactor/reactor indirectly through member functions provided by related service s to implement various io functions
  • If there is an additional need for a service, you can manually implement a service (different operating systems may require different implementations)

Other options?The author is currently at a limited level and cannot think of a better way.

Keywords: C++ socket Windows Linux Unix

Added by KDesigns on Tue, 10 Dec 2019 16:44:16 +0200