In short, consistency. The services attempt to meet user expectations set forth by the services Boost.Asio provides.
Using an internal io_service
provides a clear separation of ownership and control of handlers. If a custom service posts its internal handlers into the user's io_service
, then execution of the service's internal handlers becomes implicitly coupled with the user's handlers. Consider how this would impact user expectations with the Boost.Asio Logger Service example:
- The
logger_service
writes to the file stream within a handler. Thus, a program that never processes the io_service
event loop, such as one that only uses the synchronous API, would never have log messages written.
- The
logger_service
would no longer be thread-safe, potentially invoking undefined behavior if the io_service
is processed by multiple threads.
- The lifetime of the
logger_service
's internal operations is constrained by that of the io_service
. For example, when a service's shutdown_service()
function is invoked, the lifetime of the owning io_service
has already ended. Hence, messages could not be logged via logger_service::log()
within shutdown_service()
, as it would attempt to post an internal handler into the io_service
whose lifetime has already ended.
The user may no longer assume a one-to-one mapping between an operation and handler. For example:
boost::asio::io_service io_service;
debug_stream_socket socket(io_service);
boost::asio::async_connect(socket, ..., &connect_handler);
io_service.poll();
// Can no longer assume connect_handler has been invoked.
In this case, io_service.poll()
may invoke the handler internal to the logger_service
, rather than connect_handler()
.
Furthermore, these internal threads attempt to mimic the behavior used internally by Boost.Asio itself:
The implementation of this library for a particular platform may make use of one or more internal threads to emulate asynchronicity. As far as possible, these threads must be invisible to the library user.
In the directory monitor example, an internal thread is used to prevent indefinitely blocking the user's io_service
while waiting for an event. Once an event has occurred, the completion handler is ready to be invoked, so the internal thread post the user handler into the user's io_service
for deferred invocation. This implementation emulates asynchronicity with an internal thread that is mostly invisible to the user.
For details, when an asynchronous monitor operation is initiated via dir_monitor::async_monitor()
, a basic_dir_monitor_service::monitor_operation
is posted into the internal io_service
. When invoked, this operation invokes dir_monitor_impl::popfront_event()
, a potentially blocking call. Hence, if the monitor_operation
is posted into the user's io_service
, the user's thread could be indefinitely blocked. Consider the affect on the following code:
boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor(io_service);
dir_monitor.add_directory(dir_name);
// Post monitor_operation into io_service.
dir_monitor.async_monitor(...);
io_service.post(&user_handler);
io_service.run();
In the above code, if io_service.run()
invokes monitor_operation
first, then user_handler()
will not be invoked until dir_monitor
observes an event on the dir_name
directory. Therefore, dir_monitor
service's implementation would not behave in a consistent manner that most users expect from other services.
The use of an internal thread and io_service
:
- Mitigates the overhead of logging on the user's thread(s) by performing potentially blocking or expensive calls within the internal thread.
- Guarantees the thread-safety of
std::ofstream
, as only the single internal thread writes to the stream. If logging was done directly within logger_service::log()
or if logger_service
posted its handlers into the user's io_service
, then explicit synchronization would be required for thread-safety. Other synchronization mechanisms may introduce greater overhead or complexity into the implementation.
Allows for services
to log messages within shutdown_service()
. During destruction, the io_service
will:
- Shutdown each of its services.
- Destroy all uninvoked handlers that were scheduled for deferred invocation in the
io_service
or any of its associated strand
s.
- Destroy each of its services.
As the lifetime of the user's io_service
has ended, its event queue is neither being processed nor can additional handlers be posted. By having its own internal io_service
that is processed by its own thread, logger_service
enables other services to log messages during their shutdown_service()
.
Additional Considerations
When implementing a custom service, here are a few points to consider:
- Block all signals on internal threads.
- Never invoke the user's code directly.
- How to track and post user handlers when an implementation is destroyed.
- Resource(s) owned by the service that are shared between the service's implementations.
For the last two points, the dir_monitor
I/O object exhibits behavior that users may not expect. As the single thread within the service invokes a blocking operation on a single implementation's event queue, it effectively blocks operations that could potentially complete immediately for their respective implementation:
boost::asio::io_service io_service;
boost::asio::dir_monitor dir_monitor1(io_service);
dir_monitor1.add_directory(dir_name1);
dir_monitor1.async_monitor(&handler_A);
boost::asio::dir_monitor dir_monitor2(io_service);
dir_monitor2.add_directory(dir_name2);
dir_monitor2.async_monitor(&handler_B);
// ... Add file to dir_name2.
{
// Use scope to enforce lifetime.
boost::asio::dir_monitor dir_monitor3(io_service);
dir_monitor3.add_directory(dir_name3);
dir_monitor3.async_monitor(&handler_C);
}
io_service.run();
Although the operations associated with handler_B()
(success) and handler_C()
(aborted) would not block, the single thread in basic_dir_monitor_service
is blocked waiting for a change to dir_name1
.