It has examples.

And this what the C++11 HTTP server example does to abort a read/write loop for the client:

void connection::stop()

Exactly what I wanted to avoid. Why?

The mundane reason is that I want to have clean event logs. This is how the example(s) you're referring to "handle" errors (an excerpt):

else if (ec != boost::asio::error::operation_aborted)

The principled reason ist that I'd like that error code != 0 really means that a hard error happened, instead of "maybe error, maybe my program wants to abort the handler loop". With the example "solution" I'd be de-facto repurposing errc::invalid_handle (or whatever it's called) to mean the same as errc::operation_aborted, which  I don't like at all. If I have to explain why: because I want all the help I can get from the  OS to diagnose my own mess-ups.

Yes, I can use additional state in addition to error code, but... then I end up with  *two* things to check in each handler. (Is it an error? Did it happen because I made it happen?)

From: Boost-users <> on behalf of Gavin Lambert via Boost-users <>
Sent: Tuesday, January 30, 2018 11:08:54 PM
Cc: Gavin Lambert
Subject: Re: [Boost-users] asio: cancelling a named pipe client
On 31/01/2018 03:03, Stian Zeljko Vrba wrote:
>  > It is not valid to have more than one outstanding async read on an
> asio io object at a time
> Although I'm not doing this, this restriction is mentioned in the
> documentation only for composite operations (free-standing async_read)
> but not for members on objects (async_read_some()).

There is actually no technical restriction from having multiple pending
reads -- even for standalone async_read.  You can do it, and it will
behave "correctly".

The trouble is that this correct behaviour is not *useful* behaviour.
If you have multiple pending reads on the same object then it means the
OS is free to scatter the data some here, some there, and it becomes
impossible to make sense of the data arrival order, which renders stream
sockets fairly useless.  (It *is* something you can sensibly do with
message-based sockets, though -- but it's still unusual because
application-layer protocols usually aren't written to react well to
things being processed out of expected order.)

Multiple writes are the same -- there's no technical reason why you
can't, but usually it's nonsensical to actually do it since the data can
end up interleaved in strange and unexpected ways at the other end.

So the limit of one outstanding read and one outstanding write at a time
is a practical one, not a technical one.

> So asio leaves handling of difficult (edge-)cases to all users instead
> of offering a user-friendly opt-in solution, such as: each i/o object
> tracks posted, but not-yet-executed handlers. When cancel on the object
> is called, it would traverse the list and update error codes.

There is no list to traverse.  There can't be, due to the nature of MPMC

Besides, if an operation did actually execute correctly, it's usually
more useful to report that success even if a cancel occurred later --
after all, the bytes were actually read or transmitted, and it may be
important to know that so that you know what you need to send next.

> (Unrelated: individual operations cannot be canceled (e.g., read, but
> not write); this is a glaring design omission from my POV. I needed that
> in another project.)

This is generally an OS limitation.

It's also very standard in concurrent programming that requests to
cancel are just that: requests.  The request is free to be ignored if
the task has already completed, even if the callback hasn't been invoked
yet, and especially if the callback might already be executing.

It's simply not possible to do it any other way.

> Yes, that's another gotcha when you have outstanding both reads and
> writes. Esp. tricky to discover and fix when only, say, read, fails due
> to broken pipe, but there's no data to send so that also write() fails
> in the forseeable future. Then io_service just sits and hangs there
> waiting for the write handler to return...

If there's no data to write then you don't have a pending write to begin
with.  Write operations are only started when you actually have data to
send, and typically complete very quickly (with the exception of pipes
that are full) -- typically only read (and listen) operations are left
pending for long periods while waiting for incoming data.

> .. I guess this semi-rant can be summarized as: asio needs documentation
> on best practices/patterns/guidelines for life-time management of handlers.

It has examples.

> Does weak_ptr protect against an analogue of the "ABA" problem: Say I
> have a permanent weak_ptr to an alive shared_ptr. Then the shared_ptr
> gets destroyed. Then another shared_ptr of the same type gets created,
> but both the object and the control block get the same addresses as the
> previous instances (not unthinkable with caching allocators). How will
> lock() on the existing weak_ptr behave? Intuitively, it should return
> null, but will it? Does the standard say anything about this?

Yes, it will reliably return nullptr.  It is not possible for a new
object to have the same control block address as some prior object as
long as any weak_ptrs to the original object exist.

Essentially both the control block and the object are refcounted; a
shared_ptr holds a count of both the object and the control block, while
a weak_ptr holds a count of the control block alone.

Boost-users mailing list