Consider first that the socket is a stream and is not internally guarded against concurrent read and/or write. There are three distinct considerations.
- Concurrent execution of functions that access the same socket.
- Concurrent execution of delegates that enclose the same socket.
- Interleaved execution of delegates that write to the same socket.
The chat example is asynchronous but not concurrent. The io_service is run from a single thread, making all chat client operations non-concurrent. In other words, it avoids all of these problems. Even the async_write must internally complete sending all parts of a message before any other work can proceed, avoiding the interleaving problem.
Handlers are invoked only by a thread that is currently calling any overload of run(), run_one(), poll() or poll_one() for the io_service.
By posting work to the single thread io_service other threads can safely avoid both concurrency and blocking by queuing up work in the io_service. If however your scenario precludes you from buffering all work for a given socket, things get more complicated. You may need to block the socket communication (but not threads) as opposed to queuing up work indefinately. Also, the work queue can be very difficult to manage as it's entirely opaque.
If your io_service runs more than one thread you can still easily avoid the above problems, but you can only invoke reads or writes from the handlers of other reads or writes (and at startup). This sequences all access to the socket while remaining non-blocking. The safety arises from the fact that the pattern is using only one thread at any given time. But posting work from an independent thread is problematic - even if you don't mind buffering it.
A strand is an asio class that posts work to an io_service in a way that ensures non-concurrent invocation. However using a strand to invoke async_read and/or async_write solves only the first of the three problems. These functions internally post work to the io_service of the socket. If that service is running multiple threads the work can be exectuted concurrently.
So how do you, for a given socket, safely invoke async_read and/or async_write concurrently?
With concurrent callers the first problem can be resolved with a mutex or a strand, using the former if you don't want to buffer the work and the latter if you do. This protects the socket during the function invocations but does nothing for the other problems.
The second problem seems hardest, because it's difficult to see what's going on inside of the code executing asynchronously from the two functions. The async functions both post work to the io_service of the socket.
From the boost socket source:
/**
* This constructor creates a stream socket without opening it. The socket
* needs to be opened and then connected or accepted before data can be sent
* or received on it.
*
* @param io_service The io_service object that the stream socket will use to
* dispatch handlers for any asynchronous operations performed on the socket.
*/
explicit basic_stream_socket(boost::asio::io_service& io_service)
: basic_socket<Protocol, StreamSocketService>(io_service)
{
}
And from the io_service::run()
/**
* The run() function blocks until all work has finished and there are no
* more handlers to be dispatched, or until the io_service has been stopped.
*
* Multiple threads may call the run() function to set up a pool of threads
* from which the io_service may execute handlers. All threads that are
* waiting in the pool are equivalent and the io_service may choose any one
* of them to invoke a handler.
*
* ...
*/
BOOST_ASIO_DECL std::size_t run();
So if you give a socket multiple threads, it has no choice but to utilize multiple threads - despite not being thread safe. The only way to avoid this problem (apart from replacing the socket implementation) is to give the socket only one thread to work with. For a single socket this is what you want anyway (so don't bother running off to write a replacement).
- The third problem can be resolved by using a (different) mutex that is locked before the async_write, passed into the completion handler and unlocked at that point. This will prevent any caller from beginning a write until all parts of the preceding write are complete.
Note that the async_write posts work to a queue - that's how it is able to return almost immediately. If you throw too much work at it you may have to deal with some consequences. Despite using a single io_service thread for the socket, you may have any number of threads posting work via concurrent or non-concurrent calls to async_write.
On the other hand, async_read is straightforward. There is no interleaving problem and you simply loop back from the handler of the previous call. You may or may not want to dispatch the resulting work to another thread or queue, but if you perform it on the completion handler thread you are simply blocking all reads and writes on your single-threaded socket.
UPDATE
I did some more digging into the implementation of the underlying implementation of the socket stream (for one platform). It appears to be the case that the socket consistently executes platform socket calls on the invoking thread, not the delegate posted to the io_service. In other words, despite the fact that async_read and async_write appear to return immediately, they do in fact execute all socket operations before returning. Only the handlers are posted to the io_service. This is neither documented nor exposed by the exaple code I've reviewed, but assuming it is guaranteed behavior, it significantly impacts the second problem above.
Assuming that the work posted to the io_service does not incorporate socket operations, there is no need to limit the io_service to a single thread. It does however reinforce the importance of guarding against concurrent execution of the async functions. So, for example, if one follows the chat example but instead adds another thread to the io_service, there becomes a problem. With async function invocations executing within function handlers, you have concurrent function execution. This would require either a mutex, or all async function invocations to be reposted for execution on a strand.
UPDATE 2
With respect to the third problem (interleaving), if the data size exceeds 65536 bytes, the work is broken up internal to async_write and sent in parts. But it is critical to understand that, if there is more than one thread in the io_service, chunks of work other than the first will be posted to different threads. This all happens internal in the async_write function before your completion handler is called. The implementation creates its own intermediate completion handlers and uses them to execute all but the first socket operation.
This means any guard around the async_write call (mutex or strand) will not protect the socket if there are multiple io_service threads and more than 64kb of data to post (by default, this may possibly vary). Therefore, in this case, the interleave guard is necessary not only for interleave safety, but also thread safety of the socket. I verified all of this in a debugger.
THE MUTEX OPTION
The async_read and async_write functions internally use the io_service in order to obtain threads on which to post completion handlers, blocking until threads are available. This makes them hazardous to guard with mutex locks. When a mutex is used to guard these functions a deadlock will occur when threads back up against the lock, starving the io_service. Given that there is no other way to guard async_write when sending > 64k with a multithread io_service, it effectively locks us into a single thread in that scenario - which of course resolves the concurrency question.