1

This question is asked from the context of Boost ASIO (C++).

Say you are using a library to do some async i/o on a socket, where:

  • you are always waiting to receive data
  • you occasionally send some data

Since you are always waiting to receive data (e.g. you trigger another async_read() from your completion handler), at any given time, you will either have:

  1. an async read operation in progress
  2. an async read operation in progress and an async write operation in progress

Now say you wanted to call some other function, on_close(), when the connection closes. In Boost ASIO, a connection error or cancel() will cause any oustanding async reads/writes to give an error to your completion handler. But there is no guarantee whether you are in scenario 1. or 2., nor is there a guarantee that the write will error before the read or vice versa. So to implement this, I can only imagine adding two variables called is_reading and is_writing which are set to true by async_read() and async_write() respectively, and set to false by the completion handlers. Then, from either completion handler, when there is an error and I think the connection may be closing, I would check if there is still an async operation in the opposite direction, and call on_close() if not.

The code, more or less:

atomic_bool is_writing;
atomic_bool is_reading;

...

void read_callback(error_code& error, size_t bytes_transferred)
{
  is_reading = false;

  if (error)
  {
    if (!is_writing) on_close();
  }
  else
  {
    process_data(bytes_transferred);
  
    async_read(BUF_SIZE);  // this will set is_reading to true
  }
}

void write_callback(error_code& error, size_t bytes_transferred)
{
  is_writing = false;

  if (error)
  {
    if (!is_reading) on_close();
  }
}

Assume that this is a single-threaded app, but the thread is handling multiple sockets so you can't just let the thread end.

Is there a better way to design this? To make sure on_close() is called after the last async operation finishes?

kexu
  • 304
  • 2
  • 8

2 Answers2

1

One of the most common patterns is to use enable_shared_from_this and binding all completion handlers ("continuations") to it.

That way if the async call chain ends (be it due to error or regular completion) the shared_ptr referee will be freed.

You can see many many examples by me using Asio/Beast on this site

You can put your close logic in a destructor, or if that, too, involves async calls, you can post it on the same strand/chain.

Advanced Ideas

If your traffic is full-duplex and one side fails in a way that necessitates cancelling the other direction, you can post cancellation on the strand and the async call will abort (e.g. with error_code boost::asio::error::operation_aborted).

Even more involved would be to create a custom IO service, where the lifetime of certain "backend" entities is governed by "handle" types. This is probably often overkill, but if you are writing a foundational framework that will be used in a larger number of places, you might consider it. I think this is a good starter: How to design proper release of a boost::asio socket or wrapper thereof (be sure to follow the comment links).

sehe
  • 374,641
  • 47
  • 450
  • 633
0

You can leave error handling logic only inside read_callback.

Vladyslav Mozhvylo
  • 331
  • 1
  • 2
  • 11