`io_context.stop()` vs `socket.close()`

To close a Tcp client, which one should be used, io_context.stop() or socket.close()? What aspects should be considered when making such a choice?

As far as I know, io_context is thread-safe whereas socket is not.

So, I can invoke io_context.stop() in any thread which may be different from the one that has called io_context.run().
But for socket.close(), I need to call io_context.post([=](){socket.stop()}) if socket object is called in a different thread(e.g. the said thread calls aiso::async_read(socket, ...)).

Answer

To close a Tcp client, which one should be used, io_context.stop() or socket.close()?

Obviously socket.cancel() and or socket.shutdown() 🙂

Stopping the entire iexecution context might seem equivalent in the case of only a single IO object (your socket). But as soon as you have multiple sockets open or use timers and signal_sets, it becomes obvious why that is shooting a fly with a canon.

Also note that io_context::stop has the side effect of clearing any outstanding work (at least, inability to resume without reset() first) which makes it even more of a blunt weapon.

Instead, use socket::cancel() to cancel any IO operation on it. They will complete with error::operation_aborted so you can detect the situation. This is enough if you control all the async initiations on the object. If you want to prevent “other” parties from starting new IO operations successfully you can shutdown the socket instead. You can shutdown the writing side, reading side or both of a socket.

The reason why shutdown is often superior to close() can be quite subtle. On the one hand, shutting down one side makes it so that you can still handle/notify the other side for graceful shutdown. On the other hand there’s a prevention of a pretty common race condition when the native socket handle is (also) being stored somewhere: Closing the socket makes the native handle eligible for re-use, and a client that is unaware of the change could at a later type continue to use that handle, unaware that it now belongs to someone else. I have seen bugs in production code where under high load RPC calls would suddenly be written to the database server due to this kind of thing.

In short, best to tie the socket handle to the life time of the socket instance, and prefer to use cancel() or shutdown().

I need to call io_context.post(={socket.stop()}) if socket object is called in a different thread(e.g. the said thread calls aiso::async_read(socket, …)).

Yes, thread-safety is your responsibiliity. And no, post(io_context, ...) is not even enough when multiple threads are running the execution context. In that case you need more synchronization, like post(strand_, ...). See Why do I need strand per connection when using boost::asio?