When is the earliest an async_write completion handler will complete? - c++

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_writes 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).

Related

Create new instance of std::thread/std::jthread on every read call

I am developing a serial port program using boost::asio.
In synchronous mode I create a thread every time read_sync function is called. All reading related operation are carried in this thread (implementation is in read_sync_impl function).
On close_port or stop_read function reading operation is stopped.
This stopped reading operation can be restarted by calling the read_sync function again.
read_sync function will never be called successively without calling close_port or stop_read function in between.
I wish to know how to implement a class wide std::jthread along with proper destructor when I call my read_sync function. In languages like Kotlin or Dart the garbage-collector takes care of this. What is C++ implementation of this.
bool SerialPort::read_sync(std::uint32_t read_length, std::int32_t read_timeout)
{
this->thread_sync_read = std::jthread(&SerialPort::read_sync_impl, this);
return true;
}
bool SerialPort::read_sync_impl(const std::stop_token& st)
{
while(true)
{
...
if (st.stop_requested())
{
PLOG_INFO << "Stop Requested. Exiting thread.";
break;
}
}
}
bool SerialPort::close_port(void)
{
this->thread_sync_read->request_stop();
this->thread_sync_read->join();
this->port.close();
return this->port.is_open();
}
class SerialPort
{
public :
std::jthread *thread_sync_read = nullptr;
...
}
Actual Code
bool SerialPort::read_sync(std::uint32_t read_length, std::int32_t read_timeout)
{
try
{
if (read_timeout not_eq ignore_read_timeout)
this->read_timeout = read_timeout;//If read_timeout is not set to ignore_read_timeout, update the read_timeout else use old read_timeout
if (this->thread_sync_read.joinable())
return false; // Thread is already running
thread_sync_read = std::jthread(&SerialPort::read_sync_impl, this);
return true;
}
catch (const std::exception& ex)
{
PLOG_ERROR << ex.what();
return false;
}
}
void SerialPort::read_sync_impl(const std::stop_token& st)
{
try
{
while (true)
{
if (st.stop_requested())
{
PLOG_INFO << "Stop Requested in SerialPort::read_sync_impl. Exiting thread.";
break;
}
}
}
catch (const std::exception& ex)
{
PLOG_ERROR << ex.what();
}
}
class SerialPort
{
std::jthread thread_sync_read;
SerialPort() : io(), port(io), thread_sync_read()
{
read_buffer.fill(std::byte(0));
write_buffer.fill(std::byte(0));
}
}
You don't need to deal with the jthread's destructor. A thread object constructed without constructor arguments (default constructor), or one that has been joined, is in an empty state. This can act as a stand-in for your nullptr.
class SerialPort
{
public :
std::jthread thread_sync_read;
...
SerialPort(...)
: thread_sync_read() // no explicit constructor call needed, just for show
{}
SerialPort(SerialPort&&) = delete; // see side notes below
SerialPort& operator=(SerialPort&&) = delete;
~SerialPort()
{
if(thread_sync_read.joinable())
close_port();
}
bool read_sync(std::uint32_t read_length, std::int32_t read_timeout)
{
if(thread_sync_read.joinable())
return false; // already reading
/* start via lambda to work around parameter resolution
* issues when using member function pointer
*/
thread_sync_read = std::jthread(
[this](const std::stop_token& st) mutable {
return read_sync_impl(st);
}
);
return true;
}
bool close_port()
{
thread_sync_read.request_stop();
thread_sync_read.join(); // after this will be back in empty state
port.close();
return port.is_open();
}
};
Side notes
Starting and stopping threads is rather expensive. Normally you would want to keep a single worker thread alive and feed it new read/write requests via a work queue or something like that. But there is nothing wrong with using a simpler design like yours, especially when starting and stopping are rare operations
In the code above I delete the move constructor and assignment operator. The reason is that the thread captures the this pointer. Moving the SerialPort while the thread runs would lead to it accessing a dangling pointer
You're already reinitialize (move new one into) thread_sync_read in SerialPort::read_sync, everything should works.
at destructor, you need to remember delete read_sync
SerialPort::~SerialPort(){
close_port(); // if necessary to close port
delete thread_sync_read;
}
or if you declare thread_sync_read not as (raw) pointer
class SerialPort{
public:
std::jthread thread_sync_read;
}
then you don't need to delete it.
SerialPort::~SerialPort(){
close_port(); // if necessary
}
note that the destructor of std::jthread would perform necessary request_stop() and join() by itself.

How to ensure that the messages will be enqueued in chronological order on multithreaded Asio io_service?

Following Michael Caisse's cppcon talk I created a connection handler MyUserConnection which has a sendMessage method. sendMessage method adds a message to the queue similarly to the send() in the cppcon talk. My sendMessage method is called from multiple threads outside of the connection handler in high intervals. The messages must be enqueued chronologically.
When I run my code with only one Asio io_service::run call (aka one io_service thread) it async_write's and empties my queue as expected (FIFO), however, the problem occurs when there are, for example, 4 io_service::run calls, then the queue is not filled or the send calls are not called chronologically.
class MyUserConnection : public std::enable_shared_from_this<MyUserConnection> {
public:
MyUserConnection(asio::io_service& io_service, SslSocket socket) :
service_(io_service),
socket_(std::move(socket)),
strand_(io_service) {
}
void sendMessage(std::string msg) {
auto self(shared_from_this());
service_.post(strand_.wrap([self, msg]() {
self->queueMessage(msg);
}));
}
private:
void queueMessage(const std::string& msg) {
bool writeInProgress = !sendPacketQueue_.empty();
sendPacketQueue_.push_back(msg);
if (!writeInProgress) {
startPacketSend();
}
}
void startPacketSend() {
auto self(shared_from_this());
asio::async_write(socket_,
asio::buffer(sendPacketQueue_.front().data(), sendPacketQueue_.front().length()),
strand_.wrap([self](const std::error_code& ec, std::size_t /*n*/) {
self->packetSendDone(ec);
}));
}
void packetSendDone(const std::error_code& ec) {
if (!ec) {
sendPacketQueue_.pop_front();
if (!sendPacketQueue_.empty()) { startPacketSend(); }
} else {
// end(); // My end call
}
}
asio::io_service& service_;
SslSocket socket_;
asio::io_service::strand strand_;
std::deque<std::string> sendPacketQueue_;
};
I'm quite sure that I misinterpreted the strand and io_service::post when running the connection handler on multithreaded io_service. I'm also quite sure that the messages are not enqueued chronologically instead of messages not being async_write chronologically. How to ensure that the messages will be enqueued in chronological order in sendMessage call on multithreaded io_service?
If you use a strand, the order is guaranteed to be the order in which you post the operations to the strand.
Of course, if there is some kind of "correct ordering" between threads that post then you have to synchronize the posting between them, that's your application domain.
Here's a modernized, simplified take on your MyUserConnection class with a self-contained server test program:
Live On Coliru
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <deque>
#include <iostream>
#include <mutex>
namespace asio = boost::asio;
namespace ssl = asio::ssl;
using asio::ip::tcp;
using boost::system::error_code;
using SslSocket = ssl::stream<tcp::socket>;
class MyUserConnection : public std::enable_shared_from_this<MyUserConnection> {
public:
MyUserConnection(SslSocket&& socket) : socket_(std::move(socket)) {}
void start() {
std::cerr << "Handshake initiated" << std::endl;
socket_.async_handshake(ssl::stream_base::handshake_type::server,
[self = shared_from_this()](error_code ec) {
std::cerr << "Handshake complete" << std::endl;
});
}
void sendMessage(std::string msg) {
post(socket_.get_executor(),
[self = shared_from_this(), msg = std::move(msg)]() {
self->queueMessage(msg);
});
}
private:
void queueMessage(std::string msg) {
outbox_.push_back(std::move(msg));
if (outbox_.size() == 1)
sendLoop();
}
void sendLoop() {
std::cerr << "Sendloop " << outbox_.size() << std::endl;
if (outbox_.empty())
return;
asio::async_write( //
socket_, asio::buffer(outbox_.front()),
[this, self = shared_from_this()](error_code ec, std::size_t) {
if (!ec) {
outbox_.pop_front();
sendLoop();
} else {
end();
}
});
}
void end() {}
SslSocket socket_;
std::deque<std::string> outbox_;
};
int main() {
asio::thread_pool ioc;
ssl::context ctx(ssl::context::sslv23_server);
ctx.set_password_callback([](auto...) { return "test"; });
ctx.use_certificate_file("server.pem", ssl::context::file_format::pem);
ctx.use_private_key_file("server.pem", ssl::context::file_format::pem);
ctx.use_tmp_dh_file("dh2048.pem");
tcp::acceptor a(ioc, {{}, 8989u});
for (;;) {
auto s = a.accept(make_strand(ioc.get_executor()));
std::cerr << "accepted " << s.remote_endpoint() << std::endl;
auto sess = make_shared<MyUserConnection>(SslSocket(std::move(s), ctx));
sess->start();
for(int i = 0; i<30; ++i) {
post(ioc, [sess, i] {
std::string msg = "message #" + std::to_string(i) + "\n";
{
static std::mutex mx;
// Lock so console output is guaranteed in the same order
// as the sendMessage call
std::lock_guard lk(mx);
std::cout << "Sending " << msg << std::flush;
sess->sendMessage(std::move(msg));
}
});
}
break; // for online demo
}
ioc.join();
}
If you run it a few times, you will see that
the order in which the threads post is not deterministic (that's up to the kernel scheduling)
the order in which messages are sent (and received) is exactly the order in which they are posted.
See live demo runs on my machine:
On a multi core or even on a single core preemptive OS, you cannot truly feed messages into a queue in strictly chronological order. Even if you use a mutex to synchronize write access to the queue, the strict order is no longer guaranteed once multiple writers wait on the mutex and the mutex becomes free. At best, the order, in which the waiting write threads acquire the mutex, is implementation dependent (OS code dependent), but it is best to assume it is just random.
With that being said, the strict chronological order is a matter of definition in the first place. To explain that, imagine your PC has some digital output bits (1 for each writer thread) and you connected a logic analyzer to those bits.... and imagine, you pick some spot in the code, where you toggle such a respective bit in your enqueue function. Even if that bit toggle takes place just one assembly instruction prior to acquiring the mutex, it is possible, that the order had been changed, while the writer code approached that point. You could also set it to other arbirtrary points prior (e.g. when you enter the enqueue function). But then, the same reasoning applies. Hence, the strict chronological order is in itself a matter of definition.
There is an analogy to a case, where a CPUs interrupt controller has multiple inputs and you tried to build a system which processes those interrupts in strictly chronological order. Even if all interrupt inputs were signaled exactly at the same moment (a switch, pulling them all to signaled state simultaneously), some order would occur (e.g. caused by hardware logic or just by noise at the input pins or by the systems interrupt dispatcher function (some CPUs (e.g. MIPS 4102) have a single interrupt vector and assembly code checks the possible interrupt sources and dispatches to dedicated interrupt handlers).
This analogy helps see the pattern: It comes down to asynchronous inputs on a synchronous system. Which is a notoriously hard problem in itself.
So, the best you could possibly do, is to make a suitable definition of your applications "strict ordering" and live with it.
Then, to avoid violations of your definition, you could use a priority queue instead of a normal FIFO data type and use as priority some atomic counter:
At your chosen point in the code, atomically read and increment the counter.
This is your messages sequence number.
Assemble your message and enqueue it into the priority queue, using your sequence number as priority.
Another possible approach is to define a notion of "simultaneous", which is detectable on the other side of the queue (and thus, the reader cannot assume strict ordering for a set of "simultaneous" messages). This could be implemented by reading some high frequency tick count and all those messages, which have the same "time stamp" are to be considered simultaneous on reader side.

C++ GRPC ClientAsyncReaderWriter: how to check if data is available for read?

I have bidirectional streaming async grpc client that use ClientAsyncReaderWriter for communication with server. RPC code looks like:
rpc Process (stream Request) returns (stream Response)
For simplicity Request and Response are bytes arrays (byte[]). I send several chunks of data to server, and when server accumulate enough data, server process this data and send back the response and continue accumulating data for next responses. After several responses, the server send final response and close connection.
For async client I using CompletionQueue. Code looks like:
...
CompletionQueue cq;
std::unique_ptr<Stub> stub;
grpc::ClientContext context;
std::unique_ptr<grpc::ClientAsyncReaderWriter<Request,Response>> responder = stub->AsyncProcess(&context, &cq, handler);
// thread for completition queue
std::thread t(
[]{
void *handler = nullptr;
bool ok = false;
while (cq_.Next(&handler, &ok)) {
if (can_read) {
// how do you know that it is read data available
// Do read
} else {
// do write
...
Request request = prepare_request();
responder_->Write(request, handler);
}
}
}
);
...
// wait
What is the proper way to async reading? Can I try to read if it no data available? Is it blocking call?
Sequencing Read() calls
Can I try to read if it no data available?
Yep, and it's going to be case more often than not. Read() will do nothing until data is available, and only then put its passed tag into the completion queue. (see below for details)
Is it blocking call?
Nope. Read() and Write() return immediately. However, you can only have one of each in flight at any given moment. If you try to send a second one before the previous has completed, it (the second one) will fail.
What is the proper way to async reading?
Each time a Read() is done, start a new one. For that, you need to be able to tell when a Read() is done. This is where tags come in!
When you call Read(&msg, tag), or Write(request, tag),you are telling grpc to put tag in the completion queue associated with that responder once that operation has completed. grpc doesn't care what the tag is, it just hands it off.
So the general strategy you will want to go for is:
As soon as you are ready to start receiving messages:
call responder->Read() once with some tag that you will recognize as a "read done".
Whenever cq_.Next() gives you back that tag, and ok == true:
consume the message
Queue up a new responder->Read() with that same tag.
Obviously, you'll also want to do something similar for your calls to Write().
But since you still want to be able to lookup the handler instance from a given tag, you'll need a way to pack a reference to the handler as well as information about which operation is being finished in a single tag.
Completion queues
Lookup the handler instance from a given tag? Why?
The true raison d'ĂȘtre of completion queues is unfortunately not evident from the examples. They allow multiple asynchronous rpcs to share the same thread. Unless your application only ever makes a single rpc call, the handling thread should not be associated with a specific responder. Instead, that thread should be a general-purpose worker that dispatches events to the correct handler based on the content of the tag.
The official examples tend to do that by using pointer to the handler object as the tag. That works when there's a specific sequence of events to expect since you can easily predict what a handler is reacting to. You often can't do that with async bidirectional streams, since any given completion event could be a Read() or a Write() finishing.
Example
Here's a general outline of what I personally consider to be a clean way to go about all that:
// Base class for async bidir RPCs handlers.
// This is so that the handling thread is not associated with a specific rpc method.
class RpcHandler {
// This will be used as the "tag" argument to the various grpc calls.
struct TagData {
enum class Type {
start_done,
read_done,
write_done,
// add more as needed...
};
RpcHandler* handler;
Type evt;
};
struct TagSet {
TagSet(RpcHandler* self)
: start_done{self, TagData::Type::start_done},
read_done{self, TagData::Type::read_done},
write_done{self, TagData::Type::write_done} {}
TagData start_done;
TagData read_done;
TagData write_done;
};
public:
RpcHandler() : tags(this) {}
virtual ~RpcHandler() = default;
// The actual tag objects we'll be passing
TagSet tags;
virtual void on_ready() = 0;
virtual void on_recv() = 0;
virtual void on_write_done() = 0;
static void handling_thread_main(grpc::CompletionQueue* cq) {
void* raw_tag = nullptr;
bool ok = false;
while (cq->Next(&raw_tag, &ok)) {
TagData* tag = reinterpret_cast<TagData*>(raw_tag);
if(!ok) {
// Handle error
}
else {
switch (tag->evt) {
case TagData::Type::start_done:
tag->handler->on_ready();
break;
case TagData::Type::read_done:
tag->handler->on_recv();
break;
case TagData::Type::write_done:
tag->handler->on_write_done();
break;
}
}
}
}
};
void do_something_with_response(Response const&);
class MyHandler final : public RpcHandler {
public:
using responder_ptr =
std::unique_ptr<grpc::ClientAsyncReaderWriter<Request, Response>>;
MyHandler(responder_ptr responder) : responder_(std::move(responder)) {
// This lock is needed because StartCall() can
// cause the handler thread to access the object.
std::lock_guard lock(mutex_);
responder_->StartCall(&tags.start_done);
}
~MyHandler() {
// TODO: finish/abort the streaming rpc as appropriate.
}
void send(const Request& msg) {
std::lock_guard lock(mutex_);
if (!sending_) {
sending_ = true;
responder_->Write(msg, &tags.write_done);
} else {
// TODO: add some form of synchronous wait, or outright failure
// if the queue starts to get too big.
queued_msgs_.push(msg);
}
}
private:
// When the rpc is ready, queue the first read
void on_ready() override {
std::lock_guard l(mutex_); // To synchronize with the constructor
responder_->Read(&incoming_, &tags.read_done);
};
// When a message arrives, use it, and start reading the next one
void on_recv() override {
// incoming_ never leaves the handling thread, so no need to lock
// ------ If handling is cheap and stays in the handling thread.
do_something_with_response(incoming_);
responder_->Read(&incoming_, &tags.read_done);
// ------ If responses is expensive or involves another thread.
// Response msg = std::move(incoming_);
// responder_->Read(&incoming_, &tags.read_done);
// do_something_with_response(msg);
};
// When has been sent, send the next one is there is any
void on_write_done() override {
std::lock_guard lock(mutex_);
if (!queued_msgs_.empty()) {
responder_->Write(queued_msgs_.front(), &tags.write_done);
queued_msgs_.pop();
} else {
sending_ = false;
}
};
responder_ptr responder_;
// Only ever touched by the handler thread post-construction.
Response incoming_;
bool sending_ = false;
std::queue<Request> queued_msgs_;
std::mutex mutex_; // grpc might be thread-safe, MyHandler isn't...
};
int main() {
// Start the thread as soon as you have a completion queue.
auto cq = std::make_unique<grpc::CompletionQueue>();
std::thread t(RpcHandler::handling_thread_main, cq.get());
// Multiple concurent RPCs sharing the same handling thread:
MyHandler handler1(serviceA->MethodA(&context, cq.get()));
MyHandler handler2(serviceA->MethodA(&context, cq.get()));
MyHandlerB handler3(serviceA->MethodB(&context, cq.get()));
MyHandlerC handler4(serviceB->MethodC(&context, cq.get()));
}
If you have a keen eye, you will notice that the code above stores a bunch (1 per event type) of redundant this pointers in the handler. It's generally not a big deal, but it is possible to do without them via multiple inheritance and downcasting, but that's starting to be somewhat beyond the scope of this question.

Boost.Asio: Correct way to close socket and stop the timer and call the handler

suppose i have class X which encapsulates asio::ip::tcp::socket and deadline_timer. timer is used to break the connection if it is being too slow. Here is X:
class X
{
public:
typedef boost::function<void(const error_code&)> Handler;
void f(Handler);
void close();
private:
void on_connected(const error_code&);
void on_timeout(const error_code&);
void on_work_is_done(const error_code&);
Handler h_;
socket s_;
deadline_timer t_;
};
The function f does some work (send, receive, ...) and then calls handler like this:
void on_work_is_done(const error_code& e)
{
//swapping handlers to release handler after function exits
Handler h;
h.swap(h_);
h(e);
}
During that time the timer t is ticking. So my problem is:
what is a good way to make X::close? close() must close socket s and stop timer t, and it seems very natural that it must call handler h but only after all async operations (on s and t) are cancelled. If there wasn't timer, then the problem is solved:
void X::close() { s.close(); }
the async operations will be cancelled and on_work_is_done() will be called with err == operation_aborted, this will be passed to handler and then the handler will be released. Everything is ok. But since there are 2 objects and each of them might have some pending async operations, the problem appears to be more complex. Because if we do
void X::close()
{
s.close();
t.cancel();
}
both handlers (on_work_is_done, on_timeout) will be invoked, but we may call swapped handler_(err) from the handler that was invoked last.
Is there some straightforward solution?
I see the following approaches:
a) add size_t counter_ which will be set to 2 in X::close() and each of handlers will decrease it by 1. So the handler that made counter_ == 0 will invoke handler_(operation_aborted);
b) run t.cancel() and from on_timeout(err) if err == operation_aborted call s.cancel and from on_work_is_done() invoke handler_(operation_aborted)
Or the whole approach is bad and there is a better way?

threading-related active object design questions (c++ boost)

I would like some feedback regarding the IService class listed below. From what I know, this type of class is related to the "active-object" pattern. Please excuse/correct if I use any related terminology incorrectly. Basically the idea is that the classes using this active object class need to provide a start and a stop method which control some event loop. This event loop could be implemented with a while loop or with boost asio etc.
This class is responsible for starting a new thread in a non-blocking manner so that events can be handled in/by the new thread. It must also handle all clean-up related code. I first tried an OO approach in which subclasses were responsible for overriding methods to control the event loop but the cleanup was messy: in the destructor calling the stop method resulted in a pure virtual function call in cases where the calling class had not manually called the stop method. The templated solution seems to be a lot cleaner:
template <typename T>
class IService : private boost::noncopyable
{
typedef boost::shared_ptr<boost::thread> thread_ptr;
public:
IService()
{
}
~IService()
{
/// try stop the service in case it's running
stop();
}
void start()
{
boost::mutex::scoped_lock lock(m_threadMutex);
if (m_pServiceThread && m_pServiceThread->joinable())
{
// already running
return;
}
m_pServiceThread = thread_ptr(new boost::thread(boost::bind(&IService::main, this)));
// need to wait for thread to start: else if destructor is called before thread has started
// Wait for condition to be signaled and then
// try timed wait since the application could deadlock if the thread never starts?
//if (m_startCondition.timed_wait(m_threadMutex, boost::posix_time::milliseconds(getServiceTimeoutMs())))
//{
//}
m_startCondition.wait(m_threadMutex);
// notify main to continue: it's blocked on the same condition var
m_startCondition.notify_one();
}
void stop()
{
// trigger the stopping of the event loop
m_serviceObject.stop();
if (m_pServiceThread)
{
if (m_pServiceThread->joinable())
{
m_pServiceThread->join();
}
// the service is stopped so we can reset the thread
m_pServiceThread.reset();
}
}
private:
/// entry point of thread
void main()
{
boost::mutex::scoped_lock lock(m_threadMutex);
// notify main thread that it can continue
m_startCondition.notify_one();
// Try Dummy wait to allow 1st thread to resume???
m_startCondition.wait(m_threadMutex);
// call template implementation of event loop
m_serviceObject.start();
}
/// Service thread
thread_ptr m_pServiceThread;
/// Thread mutex
mutable boost::mutex m_threadMutex;
/// Condition for signaling start of thread
boost::condition m_startCondition;
/// T must satisfy the implicit service interface and provide a start and a stop method
T m_serviceObject;
};
The class could be used as follows:
class TestObject3
{
public:
TestObject3()
:m_work(m_ioService),
m_timer(m_ioService, boost::posix_time::milliseconds(200))
{
m_timer.async_wait(boost::bind(&TestObject3::doWork, this, boost::asio::placeholders::error));
}
void start()
{
// simple event loop
m_ioService.run();
}
void stop()
{
// signal end of event loop
m_ioService.stop();
}
void doWork(const boost::system::error_code& e)
{
// Do some work here
if (e != boost::asio::error::operation_aborted)
{
m_timer.expires_from_now( boost::posix_time::milliseconds(200) );
m_timer.async_wait(boost::bind(&TestObject3::doWork, this, boost::asio::placeholders::error));
}
}
private:
boost::asio::io_service m_ioService;
boost::asio::io_service::work m_work;
boost::asio::deadline_timer m_timer;
};
Now to my specific questions:
1) Is the use of the boost condition variable correct? It seems like a bit of a hack to me: I wanted to wait for the thread to be launched so I waited on the condition variable. Then once the new thread has launched in the main method, I again wait on the same condition variable to allow the initial thread to continue. Then once the start method of the initial thread is exited, the new thread can continue. Is this ok?
2) Are there any cases in which the thread would not get launched successfully by the OS? I remember reading somewhere that this can occur. If this is possible, I should rather do a timed wait on the condition variable (as is commented out in the start method)?
3) I am aware that of the templated class could not implement the stop method "correctly" i.e. if the event loop fails to stop, the code will block on the joins (either in the stop or in the destructor) but I see no way around this. I guess it is up to the user of the class to make sure that the start and stop method are implemented correctly?
4) I would appreciate any other design mistakes, improvements, etc?
Thanks!
Finally settled on the following:
1) After much testing use of condition variable seems fine
2) This issue hasn't cropped up (yet)
3) The templated class implementation must meet the requirements, unit tests are used to
test for correctness
4) Improvements
Added join with lock
Catching exceptions in spawned thread and rethrowing in main thread to avoid crashes and to not loose exception info
Using boost::system::error_code to communicate error codes back to caller
implementation object is set-able
Code:
template <typename T>
class IService : private boost::noncopyable
{
typedef boost::shared_ptr<boost::thread> thread_ptr;
typedef T ServiceImpl;
public:
typedef boost::shared_ptr<IService<T> > ptr;
IService()
:m_pServiceObject(&m_serviceObject)
{
}
~IService()
{
/// try stop the service in case it's running
if (m_pServiceThread && m_pServiceThread->joinable())
{
stop();
}
}
static ptr create()
{
return boost::make_shared<IService<T> >();
}
/// Accessor to service implementation. The handle can be used to configure the implementation object
ServiceImpl& get() { return m_serviceObject; }
/// Mutator to service implementation. The handle can be used to configure the implementation object
void set(ServiceImpl rServiceImpl)
{
// the implementation object cannot be modified once the thread has been created
assert(m_pServiceThread == 0);
m_serviceObject = rServiceImpl;
m_pServiceObject = &m_serviceObject;
}
void set(ServiceImpl* pServiceImpl)
{
// the implementation object cannot be modified once the thread has been created
assert(m_pServiceThread == 0);
// make sure service object is valid
if (pServiceImpl)
m_pServiceObject = pServiceImpl;
}
/// if the service implementation reports an error from the start or stop method call, it can be accessed via this method
/// NB: only the last error can be accessed
boost::system::error_code getServiceErrorCode() const { return m_ecService; }
/// The join method allows the caller to block until thread completion
void join()
{
// protect this method from being called twice (e.g. by user and by stop)
boost::mutex::scoped_lock lock(m_joinMutex);
if (m_pServiceThread && m_pServiceThread->joinable())
{
m_pServiceThread->join();
m_pServiceThread.reset();
}
}
/// This method launches the non-blocking service
boost::system::error_code start()
{
boost::mutex::scoped_lock lock(m_threadMutex);
if (m_pServiceThread && m_pServiceThread->joinable())
{
// already running
return boost::system::error_code(SHARED_INVALID_STATE, shared_category);
}
m_pServiceThread = thread_ptr(new boost::thread(boost::bind(&IService2::main, this)));
// Wait for condition to be signaled
m_startCondition.wait(m_threadMutex);
// notify main to continue: it's blocked on the same condition var
m_startCondition.notify_one();
// No error
return boost::system::error_code();
}
/// This method stops the non-blocking service
boost::system::error_code stop()
{
// trigger the stopping of the event loop
//boost::system::error_code ec = m_serviceObject.stop();
assert(m_pServiceObject);
boost::system::error_code ec = m_pServiceObject->stop();
if (ec)
{
m_ecService = ec;
return ec;
}
// The service implementation can return an error code here for more information
// However it is the responsibility of the implementation to stop the service event loop (if running)
// Failure to do so, will result in a block
// If this occurs in practice, we may consider a timed join?
join();
// If exception was thrown in new thread, rethrow it.
// Should the template implementation class want to avoid this, it should catch the exception
// in its start method and then return and error code instead
if( m_exception )
boost::rethrow_exception(m_exception);
return ec;
}
private:
/// runs in it's own thread
void main()
{
try
{
boost::mutex::scoped_lock lock(m_threadMutex);
// notify main thread that it can continue
m_startCondition.notify_one();
// Try Dummy wait to allow 1st thread to resume
m_startCondition.wait(m_threadMutex);
// call implementation of event loop
// This will block
// In scenarios where the service fails to start, the implementation can return an error code
m_ecService = m_pServiceObject->start();
m_exception = boost::exception_ptr();
}
catch (...)
{
m_exception = boost::current_exception();
}
}
/// Service thread
thread_ptr m_pServiceThread;
/// Thread mutex
mutable boost::mutex m_threadMutex;
/// Join mutex
mutable boost::mutex m_joinMutex;
/// Condition for signaling start of thread
boost::condition m_startCondition;
/// T must satisfy the implicit service interface and provide a start and a stop method
T m_serviceObject;
T* m_pServiceObject;
// Error code for service implementation errors
boost::system::error_code m_ecService;
// Exception ptr to transport exception across different threads
boost::exception_ptr m_exception;
};
Further feedback/criticism would of course be welcome.