Consider a Connection
class in a boost::asio
TCP server program that looks something like this.
#ifndef CONNECTION_HPP
#define CONNECTION_HPP
#include <iostream>
#include <boost/asio.hpp>
namespace Transmission
{
class Connection
{
public:
using SocketType = boost::asio::ip::tcp::socket;
explicit Connection(boost::asio::io_service& io_service)
: m_socket{io_service},
m_outputBuffer{},
m_writeBuffer{},
m_outputStream{&m_outputBuffer},
m_writeStream{&m_writeBuffer},
m_outputStreamPointer{&m_outputStream},
m_writeStreamPointer{&m_writeStream},
m_outputBufferPointer{&m_outputBuffer},
m_writeBufferPointer{&m_writeBuffer},
m_awaitingWrite{false},
m_pendingWrites{false}
{
}
template<typename T>
void write(const T& output)
{
*m_outputStreamPointer << output;
writeToSocket();
}
template<typename T>
std::ostream& operator<<(const T& output)
{
write(output);
m_pendingWrites = true;
return *m_outputStreamPointer;
}
std::ostream& getOutputStream()
{
writeToSocket();
m_pendingWrites = true;
return *m_outputStreamPointer;
}
void start()
{
write("Connection started");
}
SocketType& socket() { return m_socket; }
private:
void writeToSocket();
SocketType m_socket;
boost::asio::streambuf m_outputBuffer;
boost::asio::streambuf m_writeBuffer;
std::ostream m_outputStream;
std::ostream m_writeStream;
std::ostream* m_outputStreamPointer;
std::ostream* m_writeStreamPointer;
boost::asio::streambuf* m_outputBufferPointer;
boost::asio::streambuf* m_writeBufferPointer;
bool m_awaitingWrite;
bool m_pendingWrites;
};
}
#endif
Where writeToSocket
is defined as follows:
#include "Connection.hpp"
using namespace Transmission;
void Connection::writeToSocket()
{
// If a write is currently happening...
if(m_awaitingWrite)
{
// Alert the async_write's completion handler
// that writeToSocket was called while async_write was busy
// and that there is more to be written to the socket.
m_pendingWrites = true;
return;
}
// Otherwise, notify subsequent calls to this function that we're writing
m_awaitingWrite = true;
// Swap the buffers and stream pointers, so that subsequent writeToSockets
// go into the clear/old/unused buffer
std::swap(m_outputBufferPointer, m_writeBufferPointer);
std::swap(m_outputStreamPointer, m_writeStreamPointer);
// Kick off your async write, sending the front buffer.
async_write(m_socket, *m_writeBufferPointer, [this](boost::system::error_code error, std::size_t){
// The write has completed
m_awaitingWrite = false;
// If there was an error, report it.
if(error)
{
std::cout << "Async write returned an error." << std::endl;
}
else if(m_pendingWrites) // If there are more pending writes
{
// Write them
writeToSocket();
m_pendingWrites = false;
}
});
}
Incase it's not immediately obvious, the connection uses a double-buffering system to ensure that no buffer is both being async_writen
and mutated at the same time.
The piece of code I have a question regarding is:
std::ostream& getOutputStream()
{
writeToSocket(); // Kicks off an async_write, returns immediately.
m_pendingWrites = true; // Tell async_write complete handler there's more to write
return *m_outputStreamPointer; // Return the output stream for the user to insert to.
}
such that a connection can be used like: myConnection.getOutputStream() << "Hello";
Specifically, this code relies on an assumption that async_write
s completion handler will not be executed until after we return *m_outputStreamPointer
. But can we safely make that assumption?
If, for instance, the async_write completion handler completes like the following, nothing would be sent to the user:
std::ostream& getOutputStream()
{
writeToSocket(); // Kicks off an async_write, returns immediately.
// Async_write's completion handler executes.
m_pendingWrites = true; // Tell async_write complete handler there's more to write
// Completion handler already executed so m_pendingWrites = true does nothing.
return *m_outputStreamPointer; // Return the output stream for the user to insert to.
}
After looking at the documentation , I found this:
Regardless of whether the asynchronous operation completes immediately or not, the handler will not be invoked from within this function. Invocation of the handler will be performed in a manner equivalent to using boost::asio::io_service::post().
Which likely accounts for the correct behavior, but I'm not sure exactly why. Did a quick search on boost::asio::io_service::post()
but that didn't add much clarity.
Thank you, ~
The documentation bit you quote merely says that the handler will not be invoked before return on the current thread , so in a single-threaded world you have your guarantee.
However if you have multiple threads running io tasks ( io_context::run
and friends, or implicitly using thread_pool
), there is still the same race.
You can counter-act this posting all async tasks related to a connection on a strand
( Strands: Use Threads Without Explicit Locking , which is an executor that serializes all tasks posted on it (see ordering guarantees in the docs).
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.