1

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, ...)).

John
  • 2,963
  • 11
  • 33

1 Answers1

1

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?

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thank you for your detailed explanation. As per the [document](https://www.boost.org/doc/libs/1_77_0/doc/html/boost_asio/reference/basic_stream_socket/cancel/overload2.html), which says that "Calls to cancel() will always fail ... when run on Windows XP, ...and earlier versions of Windows... For portable cancellation, consider using one of the following alternatives: Use the `close()` function to simultaneously cancel the outstanding operations and close the socket." So it seems that the document suggests to call `stop()` other than `cancel()`. If I miss something, please let me know. – John Sep 19 '21 at 13:47
  • Yeah you've selectively quoted that page. It's conditional on platforms and drivers. There are multiple workarounds and you will know when the platform doesn't support cancellation. If you just duurt all these windows versions with various drivers then maybe you want to not rely on cancellation (or opt out of IOCP). That really doesn't change much about the answer since shutdown and close operations are still available. Also, in trivial scenarios io_context::stop would be just fine. – sehe Sep 19 '21 at 14:36