1

I have two async write operations using boost::asio::async_write

boost::asio::async_write(socket, boost::asio::buffer(data1), function);
boost::asio::async_write(socket, boost::asio::buffer(data2), function);

Does boost guarantee that data will be written to the socket in the exact order in which the write operations were called? In this case, I need know, is data1 will be sent before data2? If not, how can such an order be guaranteed?

Joseph Conrad
  • 43
  • 1
  • 6

1 Answers1

2

Q. Does boost guarantee that data will be written to the socket in the exact order in which the write operations were called?

No, in fact it forbids this use explicitly:

This operation is implemented in terms of zero or more calls to the stream's async_write_some function, and is known as a composed operation. The program must ensure that the stream performs no other write operations (such as async_write, the stream's async_write_some function, or any other composed operations that perform writes) until this operation completes.


Q. In this case, I need know, is data1 will be sent before data2? If not, how can such an order be guaranteed?

  • You use a strand and chained operations (starting the next async operation from the completion handler of the first).

    • A flexible way to do that without requiring a lot of tedious code is to use a queue with outbound messages

      You can look through my answers for examples

  • Alternatively using coroutines (asio::spawn or c++20's with asio::co_spawn) to hide the asynchrony:

     async_write(socket, boost::asio::buffer(data1), use_awaitable);
     async_write(socket, boost::asio::buffer(data2), use_awaitable);
    

    (use yield_context for Boost Context based stackful coroutines when you don't have c++20)

  • Lastly, keep in mind you can write buffer sequences:

     async_write(socket, std::array {
          boost::asio::buffer(data1),
          boost::asio::buffer(data2) }, function);
    

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Sorry, does boost guarantee that the asynchronous operation callback will be called on the thread where io_context.run() was called? – Joseph Conrad Jul 22 '22 at 10:44
  • 1
    Yes. If just one thread calls `run()` you have an implicit strand; you know on which thread it's going to be called. If more than one thread calls `run()` it might be any of them. – jcm Jul 22 '22 at 13:38
  • 1
    @JosephConrad yes, ["Asynchronous completion handlers will only be called from threads that are currently calling io_context::run()"](https://www.boost.org/doc/libs/1_79_0/doc/html/boost_asio/overview/core/threads.html#:~:text=Asynchronous%20completion%20handlers%20will%20only%20be%20called%20from%20threads%20that%20are%20currently%20calling%20io_context%3A%3Arun()) – sehe Jul 22 '22 at 13:47
  • 1
    Yes, adding to `jcm`'s comment, see this https://stackoverflow.com/questions/12794107/why-do-i-need-strand-per-connection-when-using-boostasio/12801042#12801042 - also note that the question code is invalid even in a single-threaded environment because the async operations can overlap – sehe Jul 22 '22 at 13:49
  • @sehe You advised me to use a queue with outbound messages. But how exactly? In the chat example on the official boost site, the queue is not executed directly in write, instead the queue is wrapped in boost::asio::post. Similarly, in this example, boost::asio::post is called when the socket is closed. Why can't we just use the queue directly instead of wrapping it in boost::asio::post? Chat client example: https://www.boost.org/doc/libs/1_70_0/doc/html/boost_asio/example/cpp11/chat/chat_client.cpp – Joseph Conrad Jul 22 '22 at 14:12
  • 1
    The answer is why I liked [this](https://stackoverflow.com/questions/12794107/why-do-i-need-strand-per-connection-when-using-boostasio/12801042#12801042). The chat example explicitly has two threads (main + `[&io_context](){ io_context.run();}`) – sehe Jul 22 '22 at 14:16
  • 1
    I also suggested some answers to look at for queueing outgoing messages. The third answer from that link https://stackoverflow.com/a/71069429 looks like a nice enough starter (didn't click on any of the other ones, so you might want to) – sehe Jul 22 '22 at 14:21
  • @sehe As I understand it, boost requires that all operations on the socket must be performed on the thread in which the run method from io_context, which this socket owns, is called? Right? If yes, can you give me boost documentation link where that moment is described? – Joseph Conrad Jul 22 '22 at 14:52
  • 1
    That's not required. The library guarantees that handlers are invoked on service threads (as quoted earlier) but you are free to call member functions where you want, **as long** as you keep in mind normal rules for data races (ordinary IO objects are [not thread-safe](https://www.boost.org/doc/libs/1_79_0/doc/html/boost_asio/reference/ip__tcp/socket.html#boost_asio.reference.ip__tcp.socket.thread_safety)). So only if you know that there is no concurrent access possible, you don't need to `post` to the strand (or otherwise perform your own locking). – sehe Jul 22 '22 at 16:00
  • 1
    [This "freedom" does not need to be documented, because the lack of of documented restriction is enough. The normal rules are described in the C++ language, e.g. https://en.cppreference.com/w/cpp/language/memory_model#Threads_and_data_races ] – sehe Jul 22 '22 at 16:01
  • @sehe Sorry, can you tell me please , it is safe to call async_write/async_read on different sockets that use the same io_context? – Joseph Conrad Jul 22 '22 at 18:03
  • 1
    Yes, because they're different instances of `socket` (see [the link above](https://www.boost.org/doc/libs/1_79_0/doc/html/boost_asio/reference/ip__tcp/socket.html#boost_asio.reference.ip__tcp.socket.thread_safety)) and `io_context` [**is** threadsafe](https://www.boost.org/doc/libs/master/doc/html/boost_asio/reference/io_context.html#boost_asio.reference.io_context.thread_safety). Note I'm linking you the documentation so you know where to look next time :) – sehe Jul 22 '22 at 20:30