Document number: P1322R0 Date: 2018-10-08 Project: Programming Language C++ Audience: SG1 - Concurrency and Parallelism, LEWG Reply-to: Christopher Kohlhoff <chris@kohlhoff.com>
At present, the Networking TS's I/O objects -- sockets, acceptors, resolvers, and timers -- can be associated only with concrete execution contexts of type io_context. This paper proposes a minor change to the specification to permit the construction of these objects with arbitrary I/O executors and execution contexts. This would:
Users and third-party libraries may implement their own execution contexts, for example to provide different scheduling guarantees, or to natively support other types of I/O objects, such as files. This proposal would allow users to create the standard I/O objects, such as sockets, to be associated with these custom execution contexts. However, although constructing the socket with a custom execution context will provide the correct, expected behaviour, there is no guarantee that it will operate as efficiently as a socket associated with a native execution context.
Some operating systems have multiple potential implementation strategies for the I/O objects, like sockets. For example, on Windows we can choose to have asynchronous notifications delivered via a completion port to a user-created thread, or we can have them delivered directly to the system thread pool. An implementer may want to make both of these strategies available, via the io_context and system_context execution contexts, respectively. For example:
io_context my_context;
tcp::socket my_socket_1(my_context); // Implementation uses a completion port.
// ...
tcp::socket my_socket_2(system_executor{}); // Implementation uses the system thread pool.
Libraries can also provide additional native execution contexts as implementation-specific extensions.
In some use cases, all asynchronous operations on a given I/O object are performed using the same executor. One common example of this is when using a strand. This proposal allows this executor to be specified once, when the I/O object is constructed. For example:
io_context my_context; // ... strand<io_context::executor_type> my_strand(my_context.get_executor()); tcp::socket my_socket(my_strand); // ... my_socket.async_receive(my_buffer, my_handler); // my_handler is invoked in the strand
A new Executor template parameter is added to the following templates:
basic_waitable_timerbasic_socketbasic_datagram_socketbasic_stream_socketbasic_socket_acceptorbasic_socket_streambufbasic_socket_iostreamip::basic_resolverTo enable convenient interoperability with arbitrary executors and execution contexts, the template parameter is defaulted to the executor polymorphic wrapper. For example:
template <class Protocol, class Executor = executor>
class basic_socket;
For each of these classes, the nested type executor_type is altered to refer to the Executor type:
template <class Protocol, class Executor>
class basic_socket : public socket_base
{
public:
using executor_type = io_context::executor_typeExecutor;
// ...
};
For each of the following templates:
basic_waitable_timerbasic_socketbasic_datagram_socketbasic_stream_socketbasic_socket_acceptorip::basic_resolverevery constructor with an io_context& parameter is replaced with two constructors:
template<class Protocol, class Executor>
class basic_socket : public socket_base
{
// ...
basic_socket(io_context& ctx, const protocol_type& protocol);
basic_socket(const executor_type& ex, const protocol_type& protocol);
template<class ExecutionContext>
basic_socket(ExecutionContext& ctx, const protocol_type& protocol);
// ...
};
The second of these constructors shall not participate in overload resolution unless is_convertible<ExecutionContext&, execution_context&>::value is true, and is_constructible<executor_type, typename ExecutionContext::executor_type>::value is true.
An OtherExecutor template parameter is added as required to the converting move constructors of the following classes:
basic_socketbasic_datagram_socketbasic_stream_socketbasic_socket_acceptorFor example:
template<class Protocol, class Executor>
class basic_socket : public socket_base
{
// ...
template<class OtherProtocol, class OtherExecutor>
basic_socket(basic_socket<OtherProtocol, OtherExecutor>&& rhs);
// ...
};
This constructor shall not participate in overload resolution unless OtherProtocol is implicitly convertible to Protocol, and OtherExecutor is implicitly convertible to Executor.
An OtherExecutor template parameter is added as required to the converting move assignment operators of the following classes:
basic_socketbasic_datagram_socketbasic_stream_socketbasic_socket_acceptorFor example:
template<class Protocol, class Executor>
class basic_socket : public socket_base
{
// ...
template<class OtherProtocol, class OtherExecutor>
basic_socket& operator=(basic_socket<OtherProtocol, OtherExecutor>&& rhs);
// ...
};
This assignment operator shall not participate in overload resolution unless OtherProtocol is implicitly convertible to Protocol, and OtherExecutor is implicitly convertible to Executor.
Every overload of member functions accept and async_accept with an io_context& parameter is replaced with two overloads:
template<class AcceptableProtocol, class Executor>
class basic_socket_acceptor : public socket_base
{
// ...
socket_type accept(io_context& ctx);
socket_type accept(const executor_type& ex);
template<class ExecutionContext>
socket_type accept(ExecutionContext& ctx);
// ...
};
The second of these overloads shall not participate in overload resolution unless is_convertible<ExecutionContext&, execution_context&>::value is true, and is_constructible<executor_type, typename ExecutionContext::executor_type>::value is true.
An Executor template parameter is added as required to the connect and async_connect functions. For example:
template<class Protocol, class Executor, class EndpointSequence>
typename Protocol::endpoint connect(basic_socket<Protocol, Executor>& s,
const EndpointSequence& endpoints);
The basic_socket_streambuf default constructor is modified so that it shall not participate in overload resolution unless is_constructible<executor_type, io_context::executor_type>::value is true.
The basic_socket_iostream specification is modified so that the executor_type type is passed to all uses of basic_socket_streambuf, as required. The basic_socket_iostream default constructor is modified so that it shall not participate in overload resolution unless is_default_constructible<basic_socket_streambuf<protocol_type, clock_type, wait_traits_type, executor_type>>::value is true.
This section is intended to provide implementation suggestions only, and is not intended to be prescriptive or exhaustive.
Implementations may use an execution context service as a container for the backend implementation.
class __socket_backend : public execution_context::service
{
// ...
};
This service would be "used" by an I/O object implementation, and is automatically created on first use:
template <class Protocol, class Executor>
class basic_socket : public socket_base
{
public:
using executor_type = Executor;
// ...
explicit basic_socket(const executor_type& ex)
{
auto& backend = use_service<__socket_backend>(ex.context());
// ...
}
// ...
};
This allows the I/O object to be used with arbitrary execution contexts.
A native execution context preemptively performs service creation by calling make_service. It can use this opportunity to pass additional constructor arguments that initialise the backend in the native mode, rather than the default:
class io_context : public execution_context
{
public:
// ...
io_context()
{
make_service<__socket_backend>(*this, __io_context_backend_tag{});
}
// ...
};
Alternatively, implementations may use a class hierarchy of services, and virtual functions, to select the desired behaviour:
class __socket_backend : public execution_context::service
{
public:
using key_type = __socket_backend;
// ...
virtual void backend_function(/* ... */);
// ...
};
class __io_socket_backend : public __socket_backend
{
public:
// ...
void backend_function(/* ... */) override;
// ...
};
class io_context : public execution_context
{
public:
// ...
io_context()
{
make_service<__io_socket_backend>(*this);
}
// ...
};
In both approaches, the existing service, with its native backend, is obtained by the I/O object constructor.
Specification of the polymorphic executor as the default I/O executor, while improving usability, has a non-zero impact on performance. This impact can be mitigated by having an I/O object's constructor detect its own well-known native executor types (e.g. by using executor::target_type). With this information, the overhead can then be limited to simple branching, rather than the memory allocations and virtual functions that would likely be required for the type-erased function objects.
Users can avoid this overhead completely by explicitly specifying the I/O executor type:
io_context my_context; basic_stream_socket<tcp, io_context::executor_type> my_socket(my_context);
This design change has been implemented in an experimental branch of Asio, for a subset of its available I/O objects.